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

原文链接: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,有三种方式:

  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 等效代码。

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

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 中, thisundefine ,但在 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 生态系统做出贡献。

相关推荐
web守墓人42 分钟前
【前端】ikun-markdown: 纯js实现markdown到富文本html的转换库
前端·javascript·html
秋田君6 小时前
深入理解JavaScript设计模式之命令模式
javascript·设计模式·命令模式
风吹落叶花飘荡7 小时前
2025 Next.js项目提前编译并在服务器
服务器·开发语言·javascript
yanlele8 小时前
我用爬虫抓取了 25 年 6 月掘金热门面试文章
前端·javascript·面试
烛阴9 小时前
WebSocket实时通信入门到实践
前端·javascript
草巾冒小子9 小时前
vue3实战:.ts文件中的interface定义与抛出、其他文件的调用方式
前端·javascript·vue.js
DoraBigHead9 小时前
你写前端按钮,他们扛服务器压力:搞懂后端那些“黑话”!
前端·javascript·架构
前端世界10 小时前
鸿蒙UI开发全解:JS与Java双引擎实战指南
javascript·ui·harmonyos
@Dream_Chaser10 小时前
uniapp ruoyi-app 中使用checkbox 无法选中问题
前端·javascript·uni-app
上单带刀不带妹10 小时前
JavaScript中的Request详解:掌握Fetch API与XMLHttpRequest
开发语言·前端·javascript·ecmascript