本文档涵盖了 Node.js 中的 ECMAScript Modules (ESM) 实现,解释了 ESM 的工作原理、如何启用、解析算法、支持的格式、特性以及与 CommonJS 的互操作性。有关 CommonJS 模块的信息,请参阅 CommonJS 模块。
ECMAScript 模块是用于打包 JavaScript 代码以供重用的官方标准格式。Node.js 已在支持其原始 CommonJS 模块格式的同时实现了对该标准的支持。ESM 在 Node.js v8.5.0 中引入,并在 v15.3.0 中稳定。
ESM 使用与 CommonJS 不同的语法,用 import 和 export 语句替换了 require() 和 module.exports。
下图说明了 Node.js 中两个模块系统之间的关系
来源:lib/internal/modules/esm/loader.js146-246 src/module_wrap.cc66-100
Node.js 拥有两个共存的模块系统:CommonJS 模块和 ECMAScript 模块。有几种方法可以告诉 Node.js 将 JavaScript 解释为 ES 模块
.mjs 扩展名package.json 中设置 "type": "module"--input-type=module同样,您也可以使用以下方法显式将文件标记为 CommonJS
.cjs 扩展名package.json 中设置 "type": "commonjs"--input-type=commonjs当没有明确的标记时,Node.js 将检查源代码中的 ES 模块语法并选择适当的模块系统。
来源:doc/api/esm.md121-140 lib/internal/modules/esm/resolve.js115-153
ESM 使用导入说明符,这是 import 语句后面用于指定加载哪个模块的字符串。有三种类型的说明符
相对说明符:以 ./、../ 或 / 开头。它们相对于导入文件解析。例如:./utils.js,../lib/helpers.mjs
裸说明符:不以 /、./、../ 开头,或不包含 URL 方案。它们通常引用 node_modules 中的包。例如:lodash,express
绝对说明符:完整的 URL,包括协议。例如:file:///opt/app/utils.js,node:fs/promises
与 CommonJS 不同,ES 模块要求相对和绝对文件说明符具有文件扩展名。目录索引(如 /index.js)也必须完整指定。
ES 模块被解析并缓存为 URL。这意味着路径中的特殊字符必须进行百分比编码。支持的 URL 方案包括
file: - 本地文件系统模块node: - 内置 Node.js 模块data: - 带有编码内容的内联模块来源:doc/api/esm.md150-192 lib/internal/modules/esm/resolve.js776-920
ESM 模块解析算法比 CommonJS 算法更复杂,因为它需要处理不同类型的说明符、包的导出/导入以及基于 URL 的解析。
其核心是,解析算法通过以下方式工作:
# 开头),还是裸说明符来源:doc/api/esm.md772-920 lib/internal/modules/esm/resolve.js195-605
Node.js ESM 支持多种模块格式
| 格式 | 描述 |
|---|---|
module | ECMAScript 模块 |
commonjs | CommonJS 模块 |
json | JSON 数据(使用 with { type: 'json' }) |
wasm | WebAssembly 模块(实验性) |
可以使用导入属性在 ESM 中导入 JSON 文件
导入 JSON 时,with { type: 'json' } 语法是必需的。JSON 模块仅公开默认导出。
当启用 --experimental-wasm-modules 标志时,可以导入 WebAssembly 模块
来源:doc/api/esm.md636-659 lib/internal/modules/esm/get_format.js24-32
可以使用模块名称或 node: URL 方案导入 Node.js 内置模块
内置模块提供命名导出和与 CommonJS 导出匹配的默认导出。
导入属性(以前称为导入断言)允许为导入指定附加元数据
当前,Node.js 仅支持 type 属性,值为 'json',用于 JSON 模块。
import.metaimport.meta 对象提供有关当前模块的元数据,包括:
import.meta.url - 模块的绝对 file: URLimport.meta.resolve(specifier) - 解析相对于当前模块的模块说明符import.meta.dirname - 当前模块的目录名(自 v21.2.0 起稳定)import.meta.filename - 完整的绝对路径和文件名(自 v21.2.0 起稳定)使用示例
awaitECMAScript 模块支持在顶层(异步函数外部)使用 await 关键字
如果顶层 await 永远不解析,Node.js 将以退出码 13 退出。
来源:doc/api/esm.md300-333 doc/api/esm.md340-420 doc/api/esm.md727-762
ESM 模块加载过程涉及多个阶段
生命周期的关键阶段包括:
ModuleJob 类协调此过程,管理模块之间的依赖关系并确保它们按正确的顺序加载。
来源: lib/internal/modules/esm/loader.js290-350 lib/internal/modules/esm/module_job.js180-250
ESM 可以导入 CommonJS 模块。从 ES 模块导入 CommonJS 模块时,module.exports 对象将作为默认导出提供。
Node.js 还会执行静态分析,尝试从 CommonJS 模块提供命名导出。
这些命名导出是尽力而为的功能,可能不适用于所有 CommonJS 模块,特别是那些导出模式复杂的模块。
导入 CommonJS 模块时,会创建一个“模块命名空间 Exotic Object”,其中包含:
module.exports 的 default 导出。module.exports 的 `'module.exports'` 命名导出。require()Node.js 现在支持通过 require() 加载同步 ES 模块。CommonJS 的 require() 函数只能加载不使用顶层 await 的 ES 模块。此功能自 Node.js v22.0.0 起可用,并且自 v23.0.0 起不再需要 --experimental-require-module 标志。
当尝试 require() 具有顶层 await 的 ESM 模块时,会抛出 ERR_REQUIRE_ASYNC_MODULE 错误。
与 CommonJS 相比,使用 ESM 的主要区别包括:
require()、exports 或 module.exports (请改用 import/export)。__filename 或 __dirname (请改用 import.meta.filename 和 import.meta.dirname)。module.createRequire() 或 process.dlopen)。require.resolve (请改用 import.meta.resolve)。如果您在 ES 模块中需要 CommonJS 功能,可以创建一个 require 函数。
来源: doc/api/esm.md460-542 doc/api/esm.md588-632 doc/api/modules.md170-214
Node.js 允许通过加载器钩子自定义模块加载过程。加载器可以修改模块的解析和加载方式。
加载器钩子系统包括:
resolve 钩子:自定义模块说明符如何解析为 URL。load 钩子:自定义模块源如何加载和解释。可以使用以下方式注册钩子:
示例解析器钩子。
示例加载器钩子。
这些钩子在与主 Node.js 进程分离的 worker 线程中运行,以提高性能和隔离性。
来源: lib/internal/modules/esm/hooks.js156-216 doc/api/module.md214-231
ESM 实现可能会抛出几个特定的错误:
| 错误代码 | 描述 |
|---|---|
ERR_REQUIRE_ESM | 尝试 require() 一个无法同步加载的 ES 模块。 |
ERR_MODULE_NOT_FOUND | 找不到请求的模块。 |
ERR_UNKNOWN_MODULE_FORMAT | 无法识别模块格式。 |
ERR_INVALID_MODULE_SPECIFIER | 模块说明符无效。 |
ERR_UNSUPPORTED_DIR_IMPORT | 尝试导入一个没有 `package.json` 和 "main" 字段的目录。 |
ERR_NETWORK_IMPORT_DISALLOWED | 尝试导入一个没有加载器的网络 URL。 |
当 ES 模块中使用顶层 `await` 且 Promise 永不解析时,Node.js 将以代码 13 退出(表示未解决的顶层 await)。
来源: lib/internal/errors.js650-740 doc/api/esm.md749-762
ECMAScript 模块是组织和分发 JavaScript 代码的标准方式。Node.js 在提供对传统 CommonJS 系统的强大支持的同时,也提供了对 ESM 的强大支持,其功能包括基于 URL 的解析、顶层 await、动态导入以及与 CommonJS 的互操作性。
Node.js 中的 ESM 实现密切遵循 ECMAScript 规范,同时为 Node.js 特定的用例(如内置模块和文件系统集成)提供了扩展。这种双模块系统允许开发者使用现代 JavaScript 功能,同时保持与现有生态系统的兼容性。