菜单

模块系统

相关源文件

Node.js 模块系统提供了组织代码成可重用单元的机制。本文档解释了 CommonJS (CJS) 模块系统(Node.js 最初的模块实现)和 ECMAScript Modules (ESM)(ES2015 中引入的标准化模块系统)的架构。

有关 node:module 模块提供的 API 方法的信息,请参阅 模块 API

模块系统概述

Node.js 支持两种模块系统

  1. CommonJS 模块 - Node.js 原生的模块格式,使用 require()module.exports
  2. ECMAScript 模块 - 标准化的 JavaScript 模块系统,使用 importexport

每种系统都有其自己的模块解析算法、缓存行为和执行语义。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 模块系统

CommonJS 是 Node.js 最初的模块系统。它使用同步模块加载,通过 require() 来加载,并通过 module.exportsexports 来暴露模块内容。

CommonJS 模块加载过程

来源: lib/internal/modules/cjs/loader.js313-326 lib/internal/modules/cjs/loader.js705-810

CommonJS 模块结构

CommonJS 中的每个模块都被包装在一个函数中,该函数提供模块特定的变量

此包装器提供

  • exports: 最初链接到 module.exports 的对象
  • require: 用于导入其他模块的函数
  • module: 代表当前模块的对象
  • __filename: 当前模块文件的完整路径
  • __dirname: 当前模块的目录名称

来源: lib/internal/modules/cjs/loader.js349-352

CommonJS 模块解析算法

模块解析算法决定了在调用 require() 时 Node.js 如何查找要加载的实际文件

  1. 如果模块是核心模块(如 'fs'),则加载内置模块
  2. 如果路径以 '/' (绝对路径) 、'./' 或 '../' (相对路径) 开头,则尝试将其作为文件或目录加载
  3. 否则,在 node_modules 目录中查找模块,从当前目录开始向上搜索
  4. 如果找不到模块,则抛出 MODULE_NOT_FOUND 错误

对于文件解析,Node.js 按以下顺序尝试这些模式

  1. <filename> 按原样
  2. <filename>.js
  3. <filename>.json
  4. <filename>.node

对于目录解析,Node.js 尝试

  1. <directory>/package.json -> main 字段
  2. <directory>/index.js
  3. <directory>/index.json
  4. <directory>/index.node

来源: lib/internal/modules/cjs/loader.js500-534 lib/internal/modules/cjs/loader.js556-566

CommonJS 模块缓存

Node.js 在加载模块后会缓存它们。后续对同一模块的 require() 调用将返回缓存的 exports 对象

这可以防止模块被多次加载和执行,但这也意味着模块状态在 require() 调用之间得以保留。

来源: lib/internal/modules/cjs/loader.js329

ECMAScript 模块

ECMAScript Modules (ESM) 是 ECMAScript 规范定义的标准化模块系统。它使用 importexport 语句,并支持异步模块加载。

ESM 模块加载过程

来源: lib/internal/modules/esm/loader.js306-350 lib/internal/modules/esm/module_job.js209-281

ESM 模块解析算法

ESM 使用比 CommonJS 更复杂的解析算法

  1. 说明符类型:

    • 相对说明符 (./foo.js)
    • 裸说明符 (express)
    • 绝对说明符 (file:///path/to/module.js)
  2. 解析过程:

    • 对于相对/绝对说明符:使用 URL 解析规则进行解析
    • 对于裸说明符:使用包解析算法
    • 解析为格式为文件 URL
  3. 包解析:

    • 检查 package.json 中的 "exports" 字段
    • 应用条件(如 "import", "node" 等)
    • 协商格式(mjs, cjs 等)
  4. 特殊功能:

    • 强制文件扩展名(与 CJS 不同)
    • 支持基于 URL 的导入
    • 支持协议处理器(file:, node:, data:)

来源: lib/internal/modules/esm/resolve.js772-878 doc/api/esm.md150-186

ESM 模块格式

ESM 可以加载不同类型的模块

格式描述
moduleJavaScript ES 模块 (.mjs 或 type:module 的 .js)
commonjs旧版 Node.js 模块 (.cjs 或 type:commonjs 的 .js)
jsonJSON 数据(需要 type:"json" 的 import 属性)
wasmWebAssembly 模块(实验性)
builtinNode.js 内置模块

来源: lib/internal/modules/esm/translators.js86-108 lib/internal/modules/esm/get_format.js18-48

ESM Import 属性

ESM 支持 import 属性(以前称为 import 断言),用于指定导入的其他元数据

目前,仅支持 type 属性,其值为 'json'

来源: doc/api/esm.md267-299

模块互操作性

Node.js 提供了 CommonJS 和 ESM 模块系统之间互操作性的机制。

ESM 中的 CommonJS 模块

当从 ESM 导入 CommonJS 模块时

  1. module.exports 对象成为默认导出
  2. 命名导出是根据 exports 对象的属性合成创建的
  3. 这些命名导出是静态的,并且不会反映加载后对 exports 对象的更改

来源: doc/api/esm.md479-536

CommonJS 中的 ESM 模块

从 CommonJS 使用 ESM 受到更多限制

  1. 只支持动态 import(),不支持静态 import 语句
  2. 可以使用 --experimental-require-module 标志来 require 同步 ES 模块
  3. 异步 ES 模块(使用顶层 await)不能同步 require

来源: doc/api/modules.md171-212 lib/internal/modules/esm/loader.js362-394

模块系统之间的主要区别

功能CommonJSESM
导入机制require()import/export
加载同步异步
文件扩展名可选强制性
thismodule.exports未定义
变量__filename, __dirnameimport.meta.url
加载 JSON原生需要导入属性
顶层 await不支持支持
循环引用部分导出实时绑定

来源: doc/api/esm.md587-633

模块钩子和自定义

Node.js 提供了钩子来定制模块加载过程。这些钩子可用于修改模块的解析和加载方式。

ESM 加载器钩子

ESM 模块加载器支持通过加载器钩子进行自定义

两个主要的钩子是

  1. Resolve 钩子:根据说明符确定模块 URL 和格式

  2. Load 钩子:加载模块源并确定其格式

来源: lib/internal/modules/esm/hooks.js72-114 doc/api/module.md214-232

模块缓存

两种模块系统都使用缓存来避免多次加载同一个模块,但它们维护着独立的缓存。

缓存对比

注意事项

  1. CommonJS 缓存:

    • 通过解析后的文件名索引
    • 可通过 require.cache 访问
    • 可以手动操作
  2. ESM 缓存:

    • 解析和加载的独立缓存
    • 基于 URL(而非文件名)
    • 无法从用户代码直接访问

来源: 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_MODULECommonJS 中的循环模块依赖

来源: lib/internal/errors.js645-1112

模块系统实现细节

V8 模块集成

Node.js 的模块系统基于 V8 的模块实现

  1. CommonJS 使用 JavaScript 函数包装器和 V8 的脚本编译
  2. ESM 使用 V8 的 ModuleWrap 和相关 API 来实现正确的 ES 模块语义

来源: src/module_wrap.cc66-147 lib/internal/modules/cjs/loader.js345-378

模块启动

在 Node.js 启动期间,模块系统按以下顺序初始化

  1. 内部绑定模块
  2. 原生模块
  3. CJS 加载器初始化
  4. ESM 加载器初始化(需要时)

这会创建一个依赖层次结构,其中较低级别的模块必须小心,避免 require 尚未可用的模块。

来源: test/parallel/test-bootstrap-modules.js24-171

未来方向

Node.js 模块系统在不断发展

  1. 改进的 ESM/CJS 互操作性:正在进行工作,以使这两种系统更好地协同工作
  2. 导入属性:提供有关导入的元数据的完全标准化的方法
  3. WebAssembly 集成:支持导入 WebAssembly 模块
  4. 模块自定义钩子:用于自定义模块加载行为的改进 API
  5. 源阶段导入:支持导入模块源以进行自定义实例化

其中一些功能是实验性的,可以通过诸如 --experimental-wasm-modules 之类的标志启用。

来源: doc/api/esm.md664-717