likes
comments
collection
share

在 Node.js 中从 CommonJS 迁移到 ECMAScript 模块(ESM)

作者站长头像
站长
· 阅读数 12

原文链接: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 中从 CommonJS 迁移到 ECMAScript 模块(ESM)

历史已经讲得够多了,所以让我们开始动手,深入了解 Node.js 中的 ECMAScript 模块。我们有很多东西要讲。

在 Node.js 中启用 ECMAScript 模块(ESM)

为了保持向后兼容性,Node.js 默认 JavaScript 代码使用 CommonJS 模块语法组织。要启用 ESM,有三种方式:

  1. 使用 .mjs 扩展(花名迈克尔·杰克逊模块(Michel's Jackson's modules))
  2. package.json 文件添加 "type": "module" 字段
  3. 使用命令行参数 --input-type=modulenode --input-type=module --eval "import { sep } from 'node:path'; console.log(sep);"

语法

ECMAScript 模块引入了新的语法。下面来看看用 CommonJS 编写的示例以及对应的 ESM 等效代码。

// util.js
module.exports.logger = (msg) => console.log(`👌 ${msg}`);

// index.js
const { logger } = require("./util");

logger("CommonJS");
// 👌 CommonJS
// 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 最吸引人的好处之一。

<srcipt type="module" src="./index.js"> </srcipt>

Sindre Sorhus“Get Ready For ESM”深入讨论了统一语法的其他好处,并鼓励包创建者转向 ESM 格式。我是再赞同不过了。

ESM 中缺少一些在 CommonJS 中存在的变量

ECMAScript 模块在运行时会缺少一些在 CommonJS 中存在的变量:

  • exports
  • module
  • __filename
  • __dirname
  • require
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 时,exportsmodule 不再需要了 。另外,其它变量我们也能额外创建。

// 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);
// Recreate missing reference to require
import { createRequire } from "module";

const require = createRequire(import.meta.url);

this 关键字的行为

值得一提的是, 两种模块语法中,this 关键字的行为在全局范围内有所不同。ESM 中, thisundefine ,但在 CommonJS 中, this 关键字指向 exports

// this keyword in ESM
console.log(this);
// undefined
// 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 函数一样。

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 函数来克服这个限制。

import data from "./data.json";
// TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".json"
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 生态系统做出贡献。