目录
文章目录隐藏
  1. JavaScript 中的模块在哪里?
  2. 多个 HTML <script>标签
  3. 脚本连接
  4. 模块加载器
  5. 模块捆绑器,预处理器和编码器
  6. ES6 模块
  7. 在浏览器中使用 ES6 模块
  8. 服务器方面的考虑
  9. 模块执行延迟
  10. 模块回退
  11. 应该在浏览器中使用模块吗?
  12. 在 Node.js 中使用 ES6 模块
  13. 你应该在 Node.js 中使用 ES6 模块吗?

这篇文章主要探索了ES6 模块,展示了它们如何在当前的转译器(transpiler)的帮助下使用。

如何理解 ES6 模块

几乎每一种语言都有一个模块的概念——一种将在一个文件中声明的功能包含在另一个文件中的方法。通常,开发人员创建一个封装好的代码库,负责处理相关任务。应用程序或其他模块可以引用该库。

好处是:

  1. 代码可以被分割成更小的自包含功能文件。
  2. 相同的模块可以在任意数量的应用程序之间共享。
  3. 理想情况下,模块永远不需要由其他开发人员检查,因为它们已经被证明是有效的。
  4. 引用模块的代码理解它是一个依赖关系。如果模块文件被更改或移动,问题就会立即显现出来。
  5. 模块代码(通常)有助于消除命名冲突。module1 中的 Functionx()不能与 module2 中的 Functionx()冲突。使用了名称空间之类的选项,因此调用 becomemodule1.x()和 module2.x()。

JavaScript 中的模块在哪里?

几年前开始 web 开发的人都会震惊地发现 JavaScript 中没有模块的概念。不可能直接引用或将一个 JavaScript 文件包含在另一个文件中。因此,开发人员诉诸于替代方案。

多个 HTML <script>标签

HTML 可以使用多个<script>标签加载任何数量的 JavaScript 文件

<script src="lib1.js"></script>
<script src="lib2.js"></script>
<script src="core.js"></script>
<script>
console.log('inline code');
</script>

在 2018 年 web 页面平均使用 25 个独立脚本,但这不是一个实用解决方案:

  1. 每个脚本启动一个新的 HTTP 请求,这会影响页面性能。HTTP/2 在一定程度上缓解了这个问题,但它不会帮助其他领域的脚本,比如 CDN。
  2. 每个脚本在运行时都会停止进一步的处理。
  3. 依赖关系管理是一个手动过程。在上面的代码中,如果 lib1.js 在 lib2.js 中引用代码,代码将失败,因为它没有加载。这可能会中断 JavaScript 的进一步处理。
  4. 除非使用适当的模块模式,否则函数可以重写其他函数。早期的 JavaScript 库使用全局函数名称或覆盖本地方法。

脚本连接

解决一个问题的多个<脚本>标记是所有 JavaScript 文件合并到一个大文件。这解决了一些性能和依赖管理问题,但可能需要手动构建和测试步骤。

模块加载器

RequireJS 和 SystemJS 等系统提供一个库,用于在运行时加载和命名空间的其他 JavaScript 库。当需要时,使用 Ajax 方法加载模块。这些系统是有帮助的,但可能成为更大的复杂代码库或网站将标准<script>标记添加到混合模式中。

模块捆绑器,预处理器和编码器

绑定器引入一个编译步骤,以便在构建时生成 JavaScript 代码。代码被处理了包括依赖和生成一个 ES5 跨浏览器兼容的连接文件。流行的选项包括 Babel、Browserify、webpack 和诸如 Grunt 和 Gulp 等更一般的任务运行程序。

JavaScript 构建过程需要一些努力,但是有一些好处:

  • 处理过程是自动化的,所以人为错误的可能性更小。
  • 进一步的处理可以剥离代码,删除调试命令,缩小结果文件等。
  • 转换允许您使用替代的语法,例如 TypeScript 或 CoffeeScript。

ES6 模块

上面的选项引入了各种相互竞争的模块定义格式。广泛适用的语法包括:

  • CommonJS -module.exports 导出接口和 require 引入模块是在 Node.js 中使用的语法
  • 异步模块定义(AMD)
  • 通用模块定义(UMD)。

因此,在 ES6(ES2015)中提出了单一的本地模块标准。

ES6 模块中的所有内容默认都是私有的,并且在严格模式下运行(不需要‘使用严格’)。使用 export 导出公共变量、函数和类。例如:

// lib.js
export const PI = 3.1415926;

export function sum(...args) {
  log('sum', args);
  return args.reduce((num, tot) => tot + num);
}

export function mult(...args) {
  log('mult', args);
  return args.reduce((num, tot) => tot * num);
}

// private function
function log(...msg) {
  console.log(...msg);
}

或者,可以使用单个导出语句。例如:

// lib.js
const PI = 3.1415926;

function sum(...args) {
  log('sum', args);
  return args.reduce((num, tot) => tot + num);
}

function mult(...args) {
  log('mult', args);
  return args.reduce((num, tot) => tot * num);
}

// private function
function log(...msg) {
  console.log(...msg);
}

export { PI, sum, mult };

然后使用 import 将项目从一个模块中提取到另一个脚本或模块中:

// main.js
import { sum } from './lib.js';
console.log( sum(1,2,3,4) ); // 10

在这种情况下,lib.js 与 main.js 在同一个文件夹中。绝对的文件引用(/),相对的文件引用(./或../)或者是完整的 url 可以被使用。

可同时导入多个项目:

import { sum, mult } from './lib.js';

console.log( sum(1,2,3,4) );  // 10
console.log( mult(1,2,3,4) ); // 24

并且可以对 imports 进行别名化以解决命名冲突:

import { sum as addAll, mult as multiplyAll } from './lib.js';

console.log( addAll(1,2,3,4) );      // 10
console.log( multiplyAll(1,2,3,4) ); // 24

最后,所有公共项目都可以通过提供一个命名空间来导入:

import * as lib from './lib.js';

console.log( lib.PI );            // 3.1415926
console.log( lib.add(1,2,3,4) );  // 10
console.log( lib.mult(1,2,3,4) ); // 24

在浏览器中使用 ES6 模块

在编写本文时,基于 chrome 的浏览器(v63+)、Safari 11+和 Edge 16+支持 ES6 模块。Firefox 支持将在版本 60 中发布(它在 v58+中的 about:config 标志后面)。

脚本使用必须通过设置一个 type=”module”属性的<script>标记来加载模块。比如:

<script type="module" src="./main.js"></script>

或内联:

<script type="module">
  import { something } from './somewhere.js';
  // ...
</script>

不管在页面或其他模块中引用了多少次,都只解析一次模块。

服务器方面的考虑

模块必须提供带有 MIME 类型 application/javascrip。大多数服务器都将自动执行此操作,但要注意动态生成的脚本或.mjs 文件

常规<script>标签可以在其他领域获取脚本但模块获取使用跨源资源共享(CORS)。因此,不同域上的模块必须设置适当的 HTTP 头,例如 Access-Control-Allow-Origin: *

最后,模块不会发送 cookie 或其他头凭证,除非 crossorigin=”use-credentials”属性添加到<script>标签和响应包含头 Access-Control-Allow-Credentials:true。

模块执行延迟

<script defer>属性延迟脚本执行,直到文件加载并解析。模块—包括内联脚本—默认延迟。例子:

<!-- runs SECOND -->
<script type="module">
  // do something...
</script>

<!-- runs THIRD -->
<script defer src="c.js"></script>

<!-- runs FIRST -->
<script src="a.js"></script>

<!-- runs FOURTH -->
<script type="module" src="b.js"></script>

模块回退

没有模块支持的浏览器不会运行 type=”module”脚本。回退脚本可以提供 nomodule 属性,模块兼容的浏览器会忽略这个属性。例如:

<script type="module" src="runs-if-module-supported.js"></script>
<script nomodule src="runs-if-module-not-supported.js"></script>

应该在浏览器中使用模块吗?

浏览器支持正在发展,但它可能有点过早转向 ES6 模块。目前,最好使用模块绑定器创建一个在任何地方都可以工作的脚本。

在 Node.js 中使用 ES6 模块

node.js 在 2009 年发布的时候,任何运行时不提供模块都是不可想象的。采用了 CommonJS,这意味着可以开发节点包管理器 npm。从那时起,使用率呈指数级增长。

CommonJS 模块的编码方式与 ES2015 模块类似。
module.exports 被使用而不是 export:

// lib.js
const PI = 3.1415926;

function sum(...args) {
  log('sum', args);
  return args.reduce((num, tot) => tot + num);
}

function mult(...args) {
  log('mult', args);
  return args.reduce((num, tot) => tot * num);
}

// private function
function log(...msg) {
  console.log(...msg);
}

module.exports = { PI, sum, mult };

require(而不是 import)用于将此模块放入另一个脚本或模块:

const { sum, mult } = require('./lib.js');

console.log( sum(1,2,3,4) );  // 10
console.log( mult(1,2,3,4) ); // 24

require 还可以导入所有项目:

const lib = require('./lib.js');

console.log( lib.PI );            // 3.1415926
console.log( lib.add(1,2,3,4) );  // 10
console.log( lib.mult(1,2,3,4) ); // 24

因此,ES6 模块易于在 Node.js 中实现吗?嗯,没有。

ES6 模块在 node.js 9.8.0+后面是一个标志,至少要到第 10 版才能完全实现。虽然 CommonJS 和 ES6 模块具有相似的语法,但它们的工作方式却截然不同:

  • ES6 模块被预先解析,以便在执行代码之前进一步解析导入。
  • CommonJS 模块在执行代码时根据需要加载依赖项。

在上述示例中没有差别,但应考虑以下 ES2015 模块代码:

// ES2015 modules

// ---------------------------------
// one.js
console.log('running one.js');
import { hello } from './two.js';
console.log(hello);

// ---------------------------------
// two.js
console.log('running two.js');
export const hello = 'Hello from two.js';

ES2015 输出结果:

running two.js
running one.js
hello from two.js

使用 CommonJS 编写的类似代码:

// CommonJS modules

// ---------------------------------
// one.js
console.log('running one.js');
const hello = require('./two.js');
console.log(hello);

// ---------------------------------
// two.js
console.log('running two.js');
module.exports = 'Hello from two.js';

CommonJS 输出结果:

running one.js
running two.js
hello from two.js

在某些应用程序中,执行顺序可能非常关键,如果 ES2015 和CommonJS 模块混合在同一个文件中会发生什么?要解决此问题,Node.js 只允许在文件中使用扩展名为.mjs 的 ES6 模块。扩展名为.js 的文件将默认为 CommonJS。这是一个简单的选项,它消除了许多复杂性,有助于代码编辑和打印。

你应该在 Node.js 中使用 ES6 模块吗?

ES6 模块从 Node.js v10 以上(2018 年 4 月发布)开始使用的。转换现有项目不太可能带来任何好处,而且会使应用程序与早期版本的 Node.js 不兼容。

对于新项目,ES6 模块提供了 CommonJS 的替代方案。语法与客户端编码相同,并且可能提供更容易的同构 JavaScript 路由,它可以在浏览器或服务器上运行。

一个标准化的 JavaScript 模块系统需要多年的时间才能实现,甚至需要更长的时间才能实现,但是问题已经得到了解决。尽管所有人都在升级的时候,应该会有一种切换的延迟,所有主流浏览器和 node.js 从 2018 年中期到现在都支持 ES6 模块。

「点点赞赏,手留余香」

6

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » 如何理解ES6模块

2 评论

  1. 楼主下次来个es6的教程

发表回复