原文链接:All you need to know to move from CommonJS to ECMAScript Modules (ESM) in Node.js,2021.05.05,by Paweł Grzybek
模块(ESM)是 ECMAScript 2015 规范引入的最具革命性的功能之一。第一个浏览器实现是在 2017 年 4 月发布的 Safari 10.1。我发表了一篇关于这个历史时刻的文章《Native ECMAScript modules in the browser》。几个月后,在 2017 年 9 月,Node v8.5.0 增加了 ESM 的实验性支持。
这个特性在实验阶段经历了大量的迭代。几年后,在 2020 年 4 月 Node v14.0.0 移除了实验警告后开始正常使用,并在 Node v14.17.0 版本中模块实现标记为稳定。
历史已经讲得够多了,所以让我们开始动手,深入了解 Node.js 中的 ECMAScript 模块。我们有很多东西要讲。
在 Node.js 中启用 ECMAScript 模块(ESM)
为了保持向后兼容性,Node.js 默认 JavaScript 代码使用 CommonJS 模块语法组织。要启用 ESM,有三种方式:
- 使用
.mjs
扩展(花名迈克尔·杰克逊模块(Michel's Jackson's modules)) package.json
文件添加"type": "module"
字段- 使用命令行参数 --input-type=module:
node --input-type=module --eval "import { sep } from 'node:path'; console.log(sep);"
语法
ECMAScript 模块引入了新的语法。下面来看看用 CommonJS 编写的示例以及对应的 ESM 等效代码。
javascript
// util.js
module.exports.logger = (msg) => console.log(`👌 ${msg}`);
// index.js
const { logger } = require("./util");
logger("CommonJS");
// 👌 CommonJS
javascript
// util.js
const logger = (msg) => console.log(`👌 ${msg}`);
export { logger };
// index.js
import { logger } from "./util.js";
logger("ECMAScript modules");
// 👌 ECMAScript modules
ESM 还是有一些语法要学习的,Node.js 也是按照官方的 ESCMAScript 模块语法来实现的。这里不多赘述,留给各位同学私下学习。另外,加载 ESM 时需要明确指定文件扩展名的(.js
或 .mjs
),这同样适用于目录索引的场景(例如 ./routes/index.js
)。
默认严格模式
ECMAScript 模块代码默认在严格模式("use strict")下运行,避免松散模式(sloppy mode)下的潜在的 BUG。
浏览器兼容性
由于 Node.js 和浏览器中的 ESM 实现遵循的是同一个官方规范,因此我们可以在服务器和客户端运行时之间共享代码。在我看来,统一的语法是使用 ESM 最吸引人的好处之一。
html
<srcipt type="module" src="./index.js"> </srcipt>
Sindre Sorhus 的"Get Ready For ESM"深入讨论了统一语法的其他好处,并鼓励包创建者转向 ESM 格式。我是再赞同不过了。
ESM 中缺少一些在 CommonJS 中存在的变量
ECMAScript 模块在运行时会缺少一些在 CommonJS 中存在的变量:
exports
module
__filename
__dirname
require
javascript
console.log(exports);
// ReferenceError: exports is not defined
console.log(module);
// ReferenceError: module is not defined
console.log(__filename);
// ReferenceError: __filename is not defined
console.log(__dirname);
// ReferenceError: __dirname is not defined
console.log(require);
// ReferenceError: require is not defined
其实在使用 ESM 时,exports
和 module
不再需要了 。另外,其它变量我们也能额外创建。
javascript
// Recreate missing reference to __filename and __dirname
import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__dirname);
console.log(__filename);
javascript
// Recreate missing reference to require
import { createRequire } from "module";
const require = createRequire(import.meta.url);
this
关键字的行为
值得一提的是, 两种模块语法中,this
关键字的行为在全局范围内有所不同。ESM 中, this
是 undefine
,但在 CommonJS 中, this
关键字指向 exports
。
javascript
// this keyword in ESM
console.log(this);
// undefined
javascript
// this keyword in CommonJS
console.log(this === module.exports);
// true
CommonJS 的动态解析到 ESM 的静态解析
CommonJS 模块在执行阶段被动态解析。这个特性就允许块作用域内使用 require
函数(例如在 if
语句中),因为依赖关系图是在程序执行期间才构建的。
ECMAScript 模块要复杂得多------在实际运行代码之前,解释器会构建一个依赖图,然后再去执行程序。预定义的依赖关系图可以让引擎执行优化,例如 tree shaking(死代码消除)等。
ESM 的顶层 await
支持
Node.js 在版本 14 中启用了对顶级 await
的支持。这稍微改变了依赖图规则,使模块像一个大的 async
函数一样。
javascript
import { promises as fs } from "fs";
// Look ma, no async function wrapper!
console.log(JSON.parse(await fs.readFile("./package.json")).type);
// module
导入 JSON
导入 JSON 是 CommonJS 中常用的功能。但在 ESM 导入 JSON 会抛出错误,我们可以通过重新创建 require
函数来克服这个限制。
javascript
import data from "./data.json";
// TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".json"
javascript
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const data = require("./data.json");
console.log(data);
// {"that": "works"}
拥抱 ESM 的最佳时机就是现在
我希望这篇文章能帮助你理解 Node.js 中 CommonJS 和 ECMAScript 模块之间的区别。我期待着有一天我们不再需要在意这些差异。整个生态系统将根据 ECMAScript 规范工作,而不管运行时(客户端或服务器)。如果你还没有,我强烈建议你现在就加入 ESM 阵营,为一致和统一的 JavaScript 生态系统做出贡献。