Node.js 模块系统完全指南:CommonJS 与 ESM 对比
Node.js 模块系统概述
Node.js 支持两套模块系统:CommonJS(CJS) 和 ES Module(ESM)。CommonJS 是 Node.js 诞生之初就使用的规范,通过 require() 和 module.exports 实现;ES Module 是 ES6 引入的官方标准,使用 import 和 export 语法。
CommonJS 规范
CommonJS 是 Node.js 默认的模块系统,每个 .js 文件都被视为一个独立的模块。
// math.js
// 命名导出(多个)
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
// 默认导出(只能有一个)
module.exports = {
add,
subtract,
};
// 同时支持命名导出和默认导出
exports.add = add;
exports.subtract = subtract;
module.exports = class Calculator { /* ... */ }; // 默认导出会覆盖上面的命名导出
// 使用 CommonJS
const { add, subtract } = require('./math');
const calc = require('./math');
// 注意:不要混用 exports.xxx 和 module.exports
// module.exports 会覆盖整个 exports 对象
ES Module 规范
ES Module 需要在 package.json 中设置 "type": "module",或使用 .mjs 扩展名。
// utils.mjs
// 命名导出(可多个)
export const name = 'Node.js';
export const version = '20.x';
// 等价写法:统一导出
const PI = 3.14159;
const E = 2.71828;
export { PI, E };
// 默认导出(每个模块只能有一个)
export default class Utils {
static greet(name) {
return `Hello, ${name}!`;
}
}
// app.mjs
// 导入命名导出
import { name, version } from './utils.mjs';
// 导入默认导出
import Utils from './utils.mjs';
// 导入时重命名
import { name as programName } from './utils.mjs';
// 同时导入命名和默认
import Utils, { name } from './utils.mjs';
// 动态导入(返回 Promise)
const module = await import('./utils.mjs');
循环依赖处理
循环依赖是 Node.js 开发中的常见陷阱。以下是正确处理方式:
// a.js
console.log('a.js 开始加载');
const { b } = require('./b');
function a() {
return '我是 A,依赖 B 的结果:' + b();
}
console.log('a.js 加载完成');
module.exports = { a };
// b.js
console.log('b.js 开始加载');
const { a } = require('./a'); // 这里拿到的是 a.js 中 require('./b') 之前的部分
function b() {
return '我是 B,但我拿不到完整的 A';
}
console.log('b.js 加载完成');
module.exports = { b };
正确的循环依赖处理原则:尽量避免循环依赖,如果无法避免,在模块加载完成后通过事件或回调传递依赖,或将共享代码提取到第三个模块中。
package.json 中的模块类型配置
{
"name": "my-app",
"type": "module", // "module" 表示使用 ESM,"commonjs"(默认)表示使用 CommonJS
"exports": {
".": {
"import": "./dist/index.mjs", // ESM 入口
"require": "./dist/index.cjs" // CommonJS 入口
},
"./utils": "./utils/index.js" // 子路径别名
},
"main": "./dist/index.cjs", // 仅用于 CommonJS
"module": "./dist/index.mjs" // bundler 使用(如 webpack、rollup)
}
CommonJS 与 ESM 核心区别
| 特性 | CommonJS | ES Module |
|------|---------|-----------|
| 语法 | require() / module.exports | import / export |
| 加载方式 | 同步,运行时加载 | 异步,编译时确定 |
| 导出 | 可修改(运行时不限制) | 导入绑定不可修改(只读) |
| 循环依赖 | 支持,但不完整 | 支持,但需要小心 |
| 顶層 this | module.exports(相当于 undefined) | undefined |
| 扩展名 | .js、.json、.node | .mjs、.js(需 package.json type=module) |
常见问题
Q1: 如何在 ESM 中导入 CommonJS 模块?
直接 import 即可,CommonJS 模块的 module.exports 会被当作 ESM 的默认导出。但命名导出(如 exports.xxx)在某些场景下不可用,需要用 import * as xxx 或 import xxx.default。
Q2: <code>import</code> 和 <code>require()</code> 能混用吗?
不能。在 ES Module 中禁止使用 require(),在 CommonJS 中也不能使用 import/export 语法(Node.js 支持通过 .mjs 文件强制使用 ESM)。建议团队统一使用一种规范。
Q3: 模块的缓存机制是怎样的?
两种模块系统都使用相同的缓存机制。模块被加载后,结果被缓存在 require.cache(CommonJS)或内部模块缓存(ESM)中。重复 require 或 import 同一模块,不会重新执行模块代码,只会返回缓存结果。
延伸阅读
- <a href="https://nodejs.org/zh-cn/docs/guides/node-core-libs/">Node.js 官方文档 - 模块系统</a>
- <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/export">ES Module 规范文档</a>
- <a href="https://nodejs.org/zh-cn/docs/guides/esm-nodejs-compat/">Node.js 18+ 原生 ESM 支持</a>