Node.js 模块系统提供了组织代码成可重用单元的机制。本文档解释了 CommonJS (CJS) 模块系统(Node.js 最初的模块实现)和 ECMAScript Modules (ESM)(ES2015 中引入的标准化模块系统)的架构。
有关 node:module 模块提供的 API 方法的信息,请参阅 模块 API。
Node.js 支持两种模块系统
require() 和 module.exportsimport 和 export每种系统都有其自己的模块解析算法、缓存行为和执行语义。Node.js 根据文件扩展名、包配置和代码内容来确定使用哪种模块系统。
Node.js 使用以下规则来确定使用哪种模块系统
| 文件类型 | 条件 | 模块系统 |
|---|---|---|
.mjs | 任意 | ESM |
.cjs | 任意 | CommonJS |
.js | 在具有 "type": "module" 的包中 | ESM |
.js | 在具有 "type": "commonjs" 或没有 type 字段的包中 | CommonJS |
.js | 没有 package.json,但包含 ESM 语法 | ESM |
| 无扩展名 | 与 require() 一起使用 | CommonJS |
| 无扩展名 | 与 import 一起使用 | ESM |
来源: doc/api/modules.md71-98 doc/api/esm.md121-140
来源: lib/internal/modules/cjs/loader.js228-243 lib/internal/modules/esm/loader.js58-99 lib/internal/modules/esm/resolve.js772-878
CommonJS 是 Node.js 最初的模块系统。它使用同步模块加载,通过 require() 来加载,并通过 module.exports 或 exports 来暴露模块内容。
来源: lib/internal/modules/cjs/loader.js313-326 lib/internal/modules/cjs/loader.js705-810
CommonJS 中的每个模块都被包装在一个函数中,该函数提供模块特定的变量
此包装器提供
exports: 最初链接到 module.exports 的对象require: 用于导入其他模块的函数module: 代表当前模块的对象__filename: 当前模块文件的完整路径__dirname: 当前模块的目录名称来源: lib/internal/modules/cjs/loader.js349-352
模块解析算法决定了在调用 require() 时 Node.js 如何查找要加载的实际文件
对于文件解析,Node.js 按以下顺序尝试这些模式
<filename> 按原样<filename>.js<filename>.json<filename>.node对于目录解析,Node.js 尝试
<directory>/package.json -> main 字段<directory>/index.js<directory>/index.json<directory>/index.node来源: lib/internal/modules/cjs/loader.js500-534 lib/internal/modules/cjs/loader.js556-566
Node.js 在加载模块后会缓存它们。后续对同一模块的 require() 调用将返回缓存的 exports 对象
这可以防止模块被多次加载和执行,但这也意味着模块状态在 require() 调用之间得以保留。
来源: lib/internal/modules/cjs/loader.js329
ECMAScript Modules (ESM) 是 ECMAScript 规范定义的标准化模块系统。它使用 import 和 export 语句,并支持异步模块加载。
来源: lib/internal/modules/esm/loader.js306-350 lib/internal/modules/esm/module_job.js209-281
ESM 使用比 CommonJS 更复杂的解析算法
说明符类型:
./foo.js)express)file:///path/to/module.js)解析过程:
包解析:
"exports" 字段特殊功能:
来源: lib/internal/modules/esm/resolve.js772-878 doc/api/esm.md150-186
ESM 可以加载不同类型的模块
| 格式 | 描述 |
|---|---|
module | JavaScript ES 模块 (.mjs 或 type:module 的 .js) |
commonjs | 旧版 Node.js 模块 (.cjs 或 type:commonjs 的 .js) |
json | JSON 数据(需要 type:"json" 的 import 属性) |
wasm | WebAssembly 模块(实验性) |
builtin | Node.js 内置模块 |
来源: lib/internal/modules/esm/translators.js86-108 lib/internal/modules/esm/get_format.js18-48
ESM 支持 import 属性(以前称为 import 断言),用于指定导入的其他元数据
目前,仅支持 type 属性,其值为 'json'。
Node.js 提供了 CommonJS 和 ESM 模块系统之间互操作性的机制。
当从 ESM 导入 CommonJS 模块时
module.exports 对象成为默认导出从 CommonJS 使用 ESM 受到更多限制
import(),不支持静态 import 语句--experimental-require-module 标志来 require 同步 ES 模块来源: doc/api/modules.md171-212 lib/internal/modules/esm/loader.js362-394
| 功能 | CommonJS | ESM |
|---|---|---|
| 导入机制 | require() | import/export |
| 加载 | 同步 | 异步 |
| 文件扩展名 | 可选 | 强制性 |
this 值 | module.exports | 未定义 |
| 变量 | __filename, __dirname | import.meta.url |
| 加载 JSON | 原生 | 需要导入属性 |
| 顶层 await | 不支持 | 支持 |
| 循环引用 | 部分导出 | 实时绑定 |
Node.js 提供了钩子来定制模块加载过程。这些钩子可用于修改模块的解析和加载方式。
ESM 模块加载器支持通过加载器钩子进行自定义
两个主要的钩子是
Resolve 钩子:根据说明符确定模块 URL 和格式
Load 钩子:加载模块源并确定其格式
来源: lib/internal/modules/esm/hooks.js72-114 doc/api/module.md214-232
两种模块系统都使用缓存来避免多次加载同一个模块,但它们维护着独立的缓存。
注意事项
CommonJS 缓存:
require.cache 访问ESM 缓存:
来源: lib/internal/modules/cjs/loader.js329-331 lib/internal/modules/esm/loader.js153-160
模块系统为不同的故障模式提供了特定的错误类型
| 错误代码 | 描述 |
|---|---|
ERR_REQUIRE_ESM | 尝试 require() 一个 ES 模块 |
ERR_IMPORT_NOT_FOUND | 动态导入无法解析 |
ERR_UNKNOWN_MODULE_FORMAT | 未知模块格式 |
ERR_UNSUPPORTED_DIR_IMPORT | 尝试导入目录 |
ERR_MODULE_NOT_FOUND | 未找到模块 |
ERR_PACKAGE_PATH_NOT_EXPORTED | 路径未从包中导出 |
ERR_INVALID_PACKAGE_CONFIG | 无效的 package.json 配置 |
ERR_REQUIRE_CYCLE_MODULE | CommonJS 中的循环模块依赖 |
来源: lib/internal/errors.js645-1112
Node.js 的模块系统基于 V8 的模块实现
来源: src/module_wrap.cc66-147 lib/internal/modules/cjs/loader.js345-378
在 Node.js 启动期间,模块系统按以下顺序初始化
这会创建一个依赖层次结构,其中较低级别的模块必须小心,避免 require 尚未可用的模块。
来源: test/parallel/test-bootstrap-modules.js24-171
Node.js 模块系统在不断发展
其中一些功能是实验性的,可以通过诸如 --experimental-wasm-modules 之类的标志启用。