CommonJS 模块是 Node.js 中最初的模块系统,它提供了一种组织和重用 JavaScript 代码的方式。本文档涵盖了 Node.js 中 CommonJS 模块系统的内部架构和机制,包括模块如何加载、缓存和解析。
有关 ECMAScript 模块 (ESM) 的信息,请参阅 ECMAScript 模块。
Node.js 将每个 JavaScript 文件视为一个独立的模块,拥有自己的作用域。CommonJS 模块系统提供了一种让这些模块使用 require() 函数来导出和导入其他模块的功能的方式。
在 CommonJS 模块系统中
module.exports 对象来公开功能require() 函数从其他模块导入功能来源:doc/api/modules.md10-69 lib/internal/modules/cjs/loader.js317-326
当 CommonJS 模块被加载时,Node.js 会将模块代码包装在一个函数中,该函数提供模块特定的上下文和私有作用域。这个包装函数提供了对几个特殊对象的访问权限。
此包装允许
module 和 exportsrequire 提供导入其他模块的方式__filename 和 __dirname 提供当前模块的文件名和目录名来源:lib/internal/modules/cjs/loader.js349-352 doc/api/modules.md38-40
require() 函数是 CommonJS 中导入模块的主要方式。它接受一个模块标识符,并返回被请求模块的 module.exports。
当你调用 require(specifier) 时,会发生以下过程:
图示:require() 函数流程
来源:lib/internal/modules/cjs/loader.js227-243 lib/internal/modules/cjs/loader.js706-807
在解析模块路径时,Node.js 会遵循特定的算法来查找正确的文件。
图示:模块解析算法
对于非核心模块,Node.js 按以下顺序解析模块:
使用解析后的路径,Node.js 接着会
来源:lib/internal/modules/cjs/loader.js706-807 doc/api/modules.md138-169
Module 类封装了 CommonJS 模块的功能。以下是其结构的高级概述:
图示:Module 类结构
Module 实例的关键属性
关键静态属性
来源:lib/internal/modules/cjs/loader.js317-345 doc/api/module.md10-17
Node.js 在首次加载模块后会缓存它。这意味着每次调用 require('foo') 都会得到完全相同的对象,前提是它解析到同一个文件。
模块缓存存储在 Module._cache 中,它是一个简单的对象,将文件名映射到模块对象。
多次调用 require('foo') 不会多次执行模块代码。相反,会返回缓存的 exports 对象。
要使模块代码多次执行,请导出函数并调用该函数。
来源:lib/internal/modules/cjs/loader.js329-331 doc/api/modules.md72-103
CommonJS 模块在其生命周期中会经历几个阶段
图示:CommonJS 模块生命周期
来源:lib/internal/modules/cjs/loader.js227-243 doc/api/modules.md38-40
CommonJS 模块系统提供两种方式从模块导出功能:exports 和 module.exports。
exports 是 module.exports 的一个引用,module.exports 最初是一个空对象。你可以向 exports 添加属性,使其在模块被 require 时可用。
但是,如果你想导出一个单独的函数、类或不同的对象,你必须直接将其赋值给 module.exports。
请记住,require() 始终返回 module.exports,而不是 exports。直接赋值给 exports 不会按预期工作。
来源: doc/api/modules.md42-67 lib/internal/modules/cjs/loader.js324-325
CommonJS 模块系统为不同的失败场景抛出特定的错误
| 错误代码 | 描述 |
|---|---|
MODULE_NOT_FOUND | 找不到模块 |
ERR_REQUIRE_ESM | 尝试 require() 一个 ES 模块 |
ERR_REQUIRE_CYCLE_MODULE | 检测到 require 调用中的循环 |
ERR_REQUIRE_ASYNC_MODULE | 尝试 require() 一个异步 ES 模块 |
当找不到模块时,错误信息将包含以下内容:
来源: lib/internal/errors.js650-684 lib/internal/modules/cjs/loader.js513-524
Node.js 允许 CommonJS 模块中的循环依赖,但它们有局限性。当两个模块相互 require 时,其中一个将获得一个部分填充的 exports 对象。
图示:循环依赖解析
处理循环依赖的方法:
来源: doc/api/modules.md350-399 lib/internal/modules/cjs/loader.js382-394
Node.js 同时支持 CommonJS 和 ECMAScript 模块。以下是主要区别:
| 功能 | CommonJS | ESM |
|---|---|---|
| 导入语法 | require() | import / import() |
| 导出语法 | exports / module.exports | export / export default |
| 文件扩展名 | .js, .cjs | .mjs, .js 配合 "type": "module" |
| 加载 | 同步 | 异步 |
| 提升 (Hoisting) | 否 | 是(导入会被提升) |
顶层 await | 否 | 是 |
| 模块解析 | 非标准自定义算法 | 基于 ECMAScript 规范 |
Node.js 根据以下内容确定文件是 CommonJS 还是 ESM:
.cjs,ESM 为 .mjs)package.json 中的 "type" 字段("commonjs" 或 "module")package.json "type",则 .js 文件为 CommonJS)从 Node.js v22.0.0 开始,CommonJS 模块可以在特定条件下 require() ESM 模块。
await。.mjs 扩展名,或者位于具有 "type": "module" 的包中。来源: doc/api/modules.md171-203 doc/api/esm.md125-144 lib/internal/modules/esm/loader.js363-379
CommonJS 模块系统主要实现在以下文件中:
lib/internal/modules/cjs/loader.js:CommonJS 模块系统的核心实现。src/node_contextify.cc:处理模块隔离所需的 V8 上下文操作。lib/internal/modules/helpers.js:模块加载的共享辅助函数。lib/internal/modules/run_main.js:处理运行主入口模块。模块加载器在 Node.js 启动过程中进行初始化。执行顺序如下:
initializeCJS() 来设置 CommonJS 模块系统。来源: lib/internal/modules/cjs/loader.js439-467 test/parallel/test-bootstrap-modules.js24-108
以下是使用 CommonJS 模块的一些最佳实践:
使用一致的导出模式。:
清晰的错误处理。:
避免修改模块系统。:
require.cache。Module._extensions。module.exports。谨慎处理循环依赖。: