CommonJS
解释 Node 传统 CommonJS 模块的导出/导入、同步加载与和 ESM 的互操作边界。
#concept
#growing
#dev
#javascript
#nodejs
[!info] related notes
- 所属:模块与工程化 MOC
- 对照:ES Module · 历史格式 · 模块模式
CommonJS
这个概念为什么出现
Node 早期用 require/module.exports 解决“文件如何变成可复用模块”。今天 ESM 是语言标准,但大量包与旧代码仍是 CJS,工程上必须会读会互操作。
[!abstract] 一句话理解 CommonJS 以同步
require加载模块,并通过exports/module.exports暴露值;它是 Node 历史默认,不是浏览器原生模块系统。
最小可运行示例
场景:把库存计价函数拆成工具模块
// pricing.cjs
function priceWithTax(base, rate) {
return base * (1 + rate)
}
module.exports = { priceWithTax }
// main.cjs
const { priceWithTax } = require('./pricing.cjs')
console.log(priceWithTax(100, 0.03))
node main.cjs
期望:103。
核心模型
- 每个文件是模块,有自己的作用域
- 首次
require通常同步执行目标 CJS 模块,后续按解析后的文件名命中缓存;不同解析路径可能得到不同实例 require返回目标的module.exports值,可以是对象、函数或原始值;不会自动复制对象exports初始指向module.exports;重绑任一变量后,两者可能不再指向同一对象- 循环依赖时可能拿到不完整导出
值、对象与绑定
// counter.cjs
exports.count = 0
exports.increment = () => { exports.count++ }
// counter-main.cjs
const counter = require('./counter.cjs')
const { count } = counter
counter.increment()
console.log(counter.count, count) // 1 0
console.log(counter === require('./counter.cjs')) // true
解构得到的数字是当时的值,counter 则仍指向同一导出对象。这与 ESM 的导入绑定跟随导出变量变化不同。
与 ESM 边界
| 点 | CJS | ESM |
|---|---|---|
| 语法 | require/exports | import/export |
| 加载 | 同步为主 | 静态分析/异步能力 |
| 浏览器 | 不原生 | 原生 type=module |
| 互操作 | 需 Node 解析规则 | 有条件导出等 |
Node 的 .cjs 明确表示 CJS,.mjs 明确表示 ESM;.js 的解释还受包配置与 Node 版本影响。新项目宜显式声明格式。
在 Node 24 中,require() 可以加载符合条件、整个依赖图不含顶层 await 的同步 ESM;异步 ESM 应使用 import()。ESM 中使用 CJS 风格的解析可通过 node:module 的 createRequire,不应把不同 Node 版本的互操作行为混为一谈。
常见误区
[!warning] 常见误区:exports = {} 重绑定 应
module.exports = {},只给exports赋值不会改变导出。
工程实践
- 新 Node 代码优先 ESM(项目策略允许时)
- 维护双包时理解
exports字段条件导出 - 打包工具会处理 CJS/ESM 转换,但不要盲目
本节总结
CJS 是 Node 历史模块合同;会它是为了生态与迁移,不是为了在新前端项目继续扩散。
自测题
- 为什么 require 有缓存?
- ESM 浏览器能否直接 require?
参考答案
- 避免重复执行,并让解析为同一文件的调用共享导出;不是所有同名包都共享一个实例。
- 不能;浏览器原生是 ESM。