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

相关推荐
王哲晓1 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
理想不理想v6 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云16 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
aPurpleBerry1 小时前
JS常用数组方法 reduce filter find forEach
javascript
ZL不懂前端2 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x2 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
我血条子呢2 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落2 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
理想不理想v3 小时前
vue经典前端面试题
前端·javascript·vue.js