菜单

模块互操作性

相关源文件

本页面解释了 Node.js 如何实现其两种模块系统:CommonJS (CJS) 和 ECMAScript 模块 (ESM) 之间的互操作性。有关每个模块系统独立工作方式的详细信息,请参阅 CommonJS 模块ECMAScript 模块

概述

Node.js 提供了机制,允许用一种格式编写的模块导入并与用另一种格式编写的模块进行交互。这种互操作性对于从旧版 CommonJS 系统向标准化 ESM 格式的过渡至关重要,它允许渐进式采用,同时保持与现有生态系统的兼容性。

来源:doc/api/esm.md459-468 doc/api/esm.md487-521 lib/internal/modules/esm/loader.js361-391

从 ESM 导入 CommonJS

ECMAScript 模块可以使用标准的 import 语法导入 CommonJS 模块。CommonJS 模块的 module.exports 作为默认导出提供,并可方便地进行命名导出。

CommonJS 命名空间

当从 ECMAScript 模块导入 CommonJS 模块时,Node.js 会构建一个包装 CommonJS module.exports 的命名空间对象。此命名空间

  1. 始终提供指向完整 module.exports 值的 default 导出
  2. 提供一个 'module.exports' 命名导出,以明确指示 CommonJS 导出值
  3. 动态分析 CommonJS 代码,以提供额外的命名导出,以提高生态系统的兼容性

来源:doc/api/esm.md479-521 doc/api/esm.md522-575

命名导出检测

Node.js 会自动检测 CommonJS 模块中的命名导出,以便在 ESM 导入中使用。此检测涵盖了常见的导出模式

但是,此检测存在限制

  • 未检测到实时绑定(导出在模块加载后的更改不会反映出来)
  • 某些动态模式可能无法正确检测
  • 导出检测是尽力而为的,并不能保证适用于所有模式

如有疑问,请使用默认导入,它始终可用且可靠。

来源:doc/api/esm.md576-585 lib/internal/modules/esm/translators.js80-82

从 CommonJS 导入 ESM

CommonJS 模块无法直接同步 require() ESM 模块。这是因为 ESM 模块可能包含异步操作(例如顶层 await),这与 require() 的同步特性不兼容。

CommonJS 中的动态导入

从 CommonJS 使用 ESM 的推荐方法是通过动态 import() 函数,该函数在两种模块系统中都有效

来源:doc/api/esm.md334-339 doc/api/modules.md176-205

实验性直接require ESM

Node.js 已添加对直接 require 符合特定条件的 ESM 模块的实验性支持

  • 模块必须是完全同步的(没有顶层 await
  • 必须通过以下方式之一识别模块
    • .mjs 文件扩展名
    • .js 扩展名,并在最近的 package.json 中设置 type: "module"
    • 源代码中的显式模块语法

此功能在 Node.js 22 和 20.19.0 中已稳定。

来源:doc/api/modules.md171-209 lib/internal/modules/esm/loader.js361-391

双向包

为了同时支持 ESM 和 CommonJS 消费者,包可以使用几种方法

包入口点

可以在 package.json 中使用 exports 字段为不同的模块系统定义不同的入口点

这使得包可以根据是使用 ESM 的 import 还是 CommonJS 的 require() 来提供不同的文件。

条件导出

可以定义更复杂的导出映射来处理不同的条件

来源:doc/api/packages.md (引用但未在提供的文件中)

互操作性挑战

不同的特性和全局变量

某些特性在一个系统中有,而在另一个系统中没有

CommonJSECMAScript 模块
require()不可用(使用 importcreateRequire
module.exports不可用(使用 export 语句)
__filename, __dirname不可用(使用 import.meta.filename, import.meta.dirname
同步加载顶层代码是同步的,但可以包含顶层 await
specifier 没有 URL 解析specifier 的基于 URL 的解析
require.cache独立的 ESM 模块缓存

来源:doc/api/esm.md589-634

误用模块系统时,可能会遇到以下错误

  • ERR_REQUIRE_ESM:尝试 require() ESM 模块
  • ERR_REQUIRE_ASYNC_MODULE:尝试 require() 带有顶层 await 的 ESM 模块
  • ERR_UNKNOWN_MODULE_FORMAT:无法确定模块的格式
  • ERR_NETWORK_IMPORT_DISALLOWED:尝试从不支持的 URL 方案导入

来源:lib/internal/modules/esm/loader.js30-29 lib/internal/errors.js183-190

同时使用两种模块系统

使用 createRequire

在 ES 模块中,module.createRequire() 函数允许您构造一个 CommonJS 风格的 require 函数

来源:doc/api/module.md50-67 doc/api/esm.md594-595

路径和 URL 的区别

ESM 模块系统使用 URL 而不是文件路径。为了保持路径的一致性

或者,较新的 Node.js 版本提供了快捷方式

来源:doc/api/esm.md596-602

实现细节

Node.js 通过创建包装器对象和执行两种系统之间的翻译来实现模块互操作性。

importSyncForRequire

ESM 加载器中的此内部方法用于 CommonJS 模块 require ES 模块时

来源:lib/internal/modules/esm/loader.js361-391 lib/internal/modules/esm/translators.js120-161

CommonJS 到 ESM 的转换

当 ESM 模块导入 CommonJS 模块时,转换层会创建一个“模块命名空间奇异对象”

来源:lib/internal/modules/esm/translators.js72-82 doc/api/esm.md487-521

最佳实践

  1. 尽可能在单个文件或包中使用单一模块格式

  2. 从 ESM 导入 CommonJS 时:

    • 使用默认导入来访问整个 module.exports 对象
    • 命名导入适用于简单情况,但不能保证适用于所有导出模式
  3. 从 CommonJS 导入 ESM 时:

    • 使用动态 import() 函数异步加载 ESM 模块
    • 构建代码以处理 import() 返回的 Promise
  4. 对于包作者:

    • 使用条件导出为不同的模块系统提供不同的入口点
    • 清晰地记录您的 API 的哪些部分适用于哪些模块系统
    • 考虑维护代码的 ESM 和 CommonJS 版本以获得最大的兼容性
  5. 对于应用程序开发者:

    • 选择一个主要模块系统,并谨慎使用互操作性功能
    • 注意文件扩展名和 package.json 中的 "type" 字段设置
    • 当您需要从 ESM 访问 CommonJS 模块时,请使用 createRequire

来源: doc/api/esm.md472-633 doc/api/modules.md171-209