菜单

CommonJS 模块

相关源文件

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 会将模块代码包装在一个函数中,该函数提供模块特定的上下文和私有作用域。这个包装函数提供了对几个特殊对象的访问权限。

此包装允许

  • 将顶层变量的作用域保留在模块内,而不是全局
  • 提供模块特定的变量,例如 moduleexports
  • 通过 require 提供导入其他模块的方式
  • 通过 __filename__dirname 提供当前模块的文件名和目录名

来源:lib/internal/modules/cjs/loader.js349-352 doc/api/modules.md38-40

require() 函数

require() 函数是 CommonJS 中导入模块的主要方式。它接受一个模块标识符,并返回被请求模块的 module.exports。

require() 如何工作

当你调用 require(specifier) 时,会发生以下过程:

图示:require() 函数流程

  1. 检查模块是否在缓存中
  2. 如果它是核心模块,则加载内置模块
  3. 使用 Module._resolveFilename() 解析文件名
  4. 使用 Module._load() 加载模块
  5. 如果需要,创建一个新的 Module 对象
  6. 调用模块的 load() 方法
  7. 包装并执行模块代码
  8. 缓存模块
  9. 返回模块的 exports

来源:lib/internal/modules/cjs/loader.js227-243 lib/internal/modules/cjs/loader.js706-807

模块解析算法

在解析模块路径时,Node.js 会遵循特定的算法来查找正确的文件。

图示:模块解析算法

对于非核心模块,Node.js 按以下顺序解析模块:

  1. 如果是绝对路径,则直接使用
  2. 如果是相对路径(以 ./ 或 ../ 开头),则相对于父模块解析
  3. 否则,在 node_modules 文件夹中搜索
    • 从当前目录的 node_modules 开始
    • 然后检查父目录的 node_modules
    • 继续向上遍历目录树
    • 最后检查全局 node_modules 路径

使用解析后的路径,Node.js 接着会

  1. 检查确切的文件名是否存在
  2. 尝试添加扩展名(.js、.json、.node)
  3. 尝试将其视为目录(查找 package.json 或 index.js)

来源:lib/internal/modules/cjs/loader.js706-807 doc/api/modules.md138-169

Module 类与内部结构

Module 类封装了 CommonJS 模块的功能。以下是其结构的高级概述:

图示:Module 类结构

Module 实例的关键属性

  • id:模块的标识符,通常是完全解析的文件名
  • path:包含模块的目录
  • exports:模块的公共接口
  • parent:首先请求此模块的模块
  • filename:模块的完全解析的文件名
  • loaded:模块是否已加载完成
  • children:此模块请求的模块

关键静态属性

  • Module._cache:所有已加载模块的缓存
  • Module._pathCache:已解析路径的缓存
  • Module._extensions:不同文件扩展名的处理函数

来源: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 模块生命周期

  1. 解析:确定模块的绝对文件路径
  2. 加载:如果未缓存,则创建一个新的 Module 实例
  3. 包装:将模块代码包装在一个函数中
  4. 求值:在模块的上下文中执行模块代码
  5. 缓存:将模块存储在缓存中以供将来 require

来源:lib/internal/modules/cjs/loader.js227-243 doc/api/modules.md38-40

exports 与 module.exports

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 对象。

图示:循环依赖解析

处理循环依赖的方法:

  1. 设计模块,使其在初始化期间不依赖于彼此的完整 exports。
  2. 导出在两个模块都加载后可以调用的函数。
  3. 考虑重构模块以避免循环依赖。

来源: doc/api/modules.md350-399 lib/internal/modules/cjs/loader.js382-394

与 ESM 的关系

Node.js 同时支持 CommonJS 和 ECMAScript 模块。以下是主要区别:

功能CommonJSESM
导入语法require()import / import()
导出语法exports / module.exportsexport / export default
文件扩展名.js, .cjs.mjs, .js 配合 "type": "module"
加载同步异步
提升 (Hoisting)是(导入会被提升)
顶层 await
模块解析非标准自定义算法基于 ECMAScript 规范

Node.js 根据以下内容确定文件是 CommonJS 还是 ESM:

  1. 文件扩展名(CommonJS 为 .cjs,ESM 为 .mjs
  2. 最近的 package.json 中的 "type" 字段("commonjs""module"
  3. 默认行为(如果没有指定 package.json "type",则 .js 文件为 CommonJS)

从 Node.js v22.0.0 开始,CommonJS 模块可以在特定条件下 require() ESM 模块。

  • ESM 模块不得使用顶层 await
  • 文件必须具有 .mjs 扩展名,或者位于具有 "type": "module" 的包中。

来源: doc/api/modules.md171-203 doc/api/esm.md125-144 lib/internal/modules/esm/loader.js363-379

内部实现

CommonJS 模块系统主要实现在以下文件中:

  1. lib/internal/modules/cjs/loader.js:CommonJS 模块系统的核心实现。
  2. src/node_contextify.cc:处理模块隔离所需的 V8 上下文操作。
  3. lib/internal/modules/helpers.js:模块加载的共享辅助函数。
  4. lib/internal/modules/run_main.js:处理运行主入口模块。

模块加载器在 Node.js 启动过程中进行初始化。执行顺序如下:

  1. 设置环境和 V8 上下文。
  2. 调用 initializeCJS() 来设置 CommonJS 模块系统。
  3. 初始化模块路径。
  4. 注册内置模块。
  5. 加载并执行主模块。

来源: lib/internal/modules/cjs/loader.js439-467 test/parallel/test-bootstrap-modules.js24-108

最佳实践

以下是使用 CommonJS 模块的一些最佳实践:

  1. 使用一致的导出模式。:

  2. 清晰的错误处理。:

  3. 避免修改模块系统。:

    • 在生产代码中不要直接操作 require.cache
    • 除非您在创建工具,否则不要修改 Module._extensions
    • 导出后不要修改 module.exports
  4. 谨慎处理循环依赖。:

    • 设计不需要完全初始化的模块的 API。
    • 使用在初始化后调用的函数导出。
    • 考虑重构以避免循环依赖。

来源: doc/api/modules.md124-156 doc/api/modules.md350-399