菜单

ECMAScript 模块

相关源文件

本文档涵盖了 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 不同的语法,用 importexport 语句替换了 require()module.exports

来源:doc/api/esm.md84-115

模块系统架构

下图说明了 Node.js 中两个模块系统之间的关系

来源:lib/internal/modules/esm/loader.js146-246 src/module_wrap.cc66-100

启用 ESM

Node.js 拥有两个共存的模块系统:CommonJS 模块和 ECMAScript 模块。有几种方法可以告诉 Node.js 将 JavaScript 解释为 ES 模块

  1. 文件扩展名:使用 .mjs 扩展名
  2. 包配置:在最近的父级 package.json 中设置 "type": "module"
  3. CLI 标志:在运行 Node.js 时使用 --input-type=module

同样,您也可以使用以下方法显式将文件标记为 CommonJS

  1. 文件扩展名:使用 .cjs 扩展名
  2. 包配置:在最近的父级 package.json 中设置 "type": "commonjs"
  3. CLI 标志:使用 --input-type=commonjs

当没有明确的标记时,Node.js 将检查源代码中的 ES 模块语法并选择适当的模块系统。

来源:doc/api/esm.md121-140 lib/internal/modules/esm/resolve.js115-153

导入说明符

ESM 使用导入说明符,这是 import 语句后面用于指定加载哪个模块的字符串。有三种类型的说明符

  1. 相对说明符:以 ./..// 开头。它们相对于导入文件解析。例如:./utils.js../lib/helpers.mjs

  2. 裸说明符:不以 /./../ 开头,或不包含 URL 方案。它们通常引用 node_modules 中的包。例如:lodashexpress

  3. 绝对说明符:完整的 URL,包括协议。例如:file:///opt/app/utils.jsnode:fs/promises

强制文件扩展名

与 CommonJS 不同,ES 模块要求相对和绝对文件说明符具有文件扩展名。目录索引(如 /index.js)也必须完整指定。

ESM 中的 URL

ES 模块被解析并缓存为 URL。这意味着路径中的特殊字符必须进行百分比编码。支持的 URL 方案包括

  • file: - 本地文件系统模块
  • node: - 内置 Node.js 模块
  • data: - 带有编码内容的内联模块

来源:doc/api/esm.md150-192 lib/internal/modules/esm/resolve.js776-920

解析算法

ESM 模块解析算法比 CommonJS 算法更复杂,因为它需要处理不同类型的说明符、包的导出/导入以及基于 URL 的解析。

其核心是,解析算法通过以下方式工作:

  1. 确定说明符是 URL、相对路径、包导入说明符(以 # 开头),还是裸说明符
  2. 将说明符解析为 URL
  3. 验证 URL(检查编码的路径分隔符)
  4. 检查文件是否存在
  5. 确定模块的格式
  6. 返回解析后的 URL 和格式

来源:doc/api/esm.md772-920 lib/internal/modules/esm/resolve.js195-605

模块格式

Node.js ESM 支持多种模块格式

格式描述
moduleECMAScript 模块
commonjsCommonJS 模块
jsonJSON 数据(使用 with { type: 'json' }
wasmWebAssembly 模块(实验性)

JSON 模块

可以使用导入属性在 ESM 中导入 JSON 文件

导入 JSON 时,with { type: 'json' } 语法是必需的。JSON 模块仅公开默认导出。

WebAssembly 模块(实验性)

当启用 --experimental-wasm-modules 标志时,可以导入 WebAssembly 模块

来源:doc/api/esm.md636-659 lib/internal/modules/esm/get_format.js24-32

ESM 特性

内置模块

可以使用模块名称或 node: URL 方案导入 Node.js 内置模块

内置模块提供命名导出和与 CommonJS 导出匹配的默认导出。

导入属性

导入属性(以前称为导入断言)允许为导入指定附加元数据

当前,Node.js 仅支持 type 属性,值为 'json',用于 JSON 模块。

import.meta

import.meta 对象提供有关当前模块的元数据,包括:

  • import.meta.url - 模块的绝对 file: URL
  • import.meta.resolve(specifier) - 解析相对于当前模块的模块说明符
  • import.meta.dirname - 当前模块的目录名(自 v21.2.0 起稳定)
  • import.meta.filename - 完整的绝对路径和文件名(自 v21.2.0 起稳定)

使用示例

顶层 await

ECMAScript 模块支持在顶层(异步函数外部)使用 await 关键字

如果顶层 await 永远不解析,Node.js 将以退出码 13 退出。

来源:doc/api/esm.md300-333 doc/api/esm.md340-420 doc/api/esm.md727-762

模块加载生命周期

ESM 模块加载过程涉及多个阶段

生命周期的关键阶段包括:

  1. 解析:将说明符解析为 URL
  2. 加载:加载模块源代码
  3. 实例化:实例化模块并链接其依赖项
  4. 评估:执行模块代码

ModuleJob 类协调此过程,管理模块之间的依赖关系并确保它们按正确的顺序加载。

来源: lib/internal/modules/esm/loader.js290-350 lib/internal/modules/esm/module_job.js180-250

与 CommonJS 的互操作性

在 ESM 中导入 CommonJS

ESM 可以导入 CommonJS 模块。从 ES 模块导入 CommonJS 模块时,module.exports 对象将作为默认导出提供。

Node.js 还会执行静态分析,尝试从 CommonJS 模块提供命名导出。

这些命名导出是尽力而为的功能,可能不适用于所有 CommonJS 模块,特别是那些导出模式复杂的模块。

CommonJS 命名空间

导入 CommonJS 模块时,会创建一个“模块命名空间 Exotic Object”,其中包含:

  • 一个指向 module.exportsdefault 导出。
  • 一个指向 module.exports 的 `'module.exports'` 命名导出。
  • 基于 CommonJS 模块静态分析的命名导出。

在 ESM 中使用 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 的区别

与 CommonJS 相比,使用 ESM 的主要区别包括:

  • 没有 require()exportsmodule.exports (请改用 import/export)。
  • 没有 __filename__dirname (请改用 import.meta.filenameimport.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 功能,同时保持与现有生态系统的兼容性。