理解 ES6 模块

大约 9 分钟javascriptes6modules

理解 ES6 模块

本文探讨 ES6 模块,展示如何通过转译器在今天使用它们。

几乎每种编程语言都有模块的概念,即在一个文件中声明的功能可以被另一个文件使用。通常,开发者会封装一个代码库,用来完成特定功能。这个库可以被应用或其他模块引用。

使用模块的好处:

  1. 代码可以分割成包含自包含功能的小文件。
  2. 相同的模块可以在任意的应用程序之间共享。
  3. 在理想情况下,其他开发者不需要关心模块内部,因为它们可以直接使用。
  4. 被引用模块是一个依赖。如果模块文件被更改或移动,马上就会发现问题。
  5. 模块代码(通常)有助于消除命名冲突。模块1中的函数 x() 不会与模块2中的函数 x() 冲突。使用名称空间等选项,使调用变成 module1.x() 和 module2.x()

JavaScript 模块在哪里?

几年前开始网页开发的任何人都会惊讶地发现 JavaScript 中没有模块的概念。不可能在另一个 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>

然而,这不是一个实用的解决方案:

  • 每个脚本启动一个新的 HTTP 请求,这会影响页面性能。 HTTP/2 在一定程度上缓解了这个问题,但它不能帮助引用其他域上的脚本,如 CDN。
  • 每个脚本在运行时会阻止进一步处理。
  • 依赖管理是一个手动过程。在上面的代码中,如果 lib1.js 引用 lib2.js 中的代码,则代码将失败,因为它尚未被加载。这可能破坏进一步的 JavaScript 处理。
  • 函数可能会覆盖其他函数,除非使用适当的 模块模式open in new window。早期的 JavaScript 库以使用全局函数名称或覆盖本地方法而臭名昭著。

脚本串接

解决多个 <script> 标记问题的一个方法是将所有 JavaScript 文件连接到单个大文件中。这解决了一些性能和依赖关系管理问题,但可能需要手动构建和测试步骤。

模块加载器

系统,如 RequireJSopen in new window 和 SystemJSopen in new window,提供了一个加载和使用其他 JavaScript 库的运行时库。模块在需要时使用 Ajax 方法加载。系统可以提供帮助,但对于大型代码库或添加了标准 <script> 标记的网站来说,可能变得复杂。

模块绑定程序、预处理器和转译器

绑定程序引入了一个编译步骤,因此 JavaScript 代码在构建时生成。处理代码以包含依赖关系并产生一个单个的 ES5 跨浏览器兼容连续文件。流行的选择包括 Babelopen in new window、 Browserifyopen in new window、 webpackopen in new window 以及更通用的任务运行器,如 Gruntopen in new window 和 Gulpopen in new window

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

ES6 模块

上述选项引入了多种竞争的模块定义格式。广泛采用的语法包括:

  • CommonJS —— Node.js 中使用的 module.exports 和 require 语法
  • 异步模块定义 (AMD)
  • 通用模块定义 (UMD)。

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

ES6 模块内部的所有内容默认都是私有的,并在严格模式下运行(不需要 'use strict')。使用 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);
}

// 私有函数
function log(...msg) {
  console.log(...msg);
}

或者,可以使用单个 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);
}

// 私有函数
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

并可以将导入别名以解决命名冲突:

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 模块

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

使用模块的脚本必须通过在 <script> 标记中设置 type="module" 属性来加载。例如:

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

或内联:

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

模块解析一次,无论它们在页面或其他模块中被引用多少次。

服务器考虑

模块必须使用 MIME 类型 application/javascript 服务。大多数服务器会自动执行此操作,但要注意动态生成的脚本或 .mjs 文件(请参见下面的 Node.js 部分open in new window)。

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

最后,模块不会发送 cookie 或其他标头凭据,除非在 <script> 标记中添加 crossorigin="use-credentials" 属性,并且响应包含标头 Access-Control-Allow-Credentials: true

模块执行被延迟

<script defer> 属性将脚本执行延迟到文档加载和解析之后。模块,包括内联脚本,默认推迟。例如:

<!-- 第二运行 -->
<script type="module">
  // do something...
</script><!-- 第三运行 -->
<script defer src="c.js"></script><!-- 第一运行 -->
<script src="a.js"></script><!-- 第四运行 -->
<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,这意味着可以开发 Node 包管理器 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);
}

// 私有函数
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

那么在Node.js中实现ES6模块很容易,对吗? 呃,不是

ES6模块在Node.js 9.8.0+中**需要打开标志open in new window**,并且至少要到版本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。这是一个简单的选项,消除了大部分复杂性,应该有助于代码编辑器和linters。

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

ES6模块仅适用于Node.js v10及以上版本(于2018年4月发布)。转换现有项目不太可能产生任何好处,并且会使应用程序与早期版本的Node.js不兼容。

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

模块搏斗

标准化的JavaScript模块系统花费了很多年时间才到达,甚至更长时间才实现,但问题已经得到解决。所有主流浏览器和从2018年中期开始的Node.js都支持ES6模块,尽管应该预期切换期滞后,直到所有人都升级为止。

今天学习ES6模块,明天从中受益,提高你的JavaScript开发。

上次编辑于:
贡献者: mickmetalholic,Shen Yuan