Node.js 中 CommonJS 和 ES6 模块的区别
1. 语法差异
CommonJS (CJS)
// 导出
module.exports = { a: 1, b: 2 };
// 或
exports.a = 1;
exports.b = 2;
// 导入
const { a, b } = require('./module');
const module = require('./module');
ES6 模块 (ESM)
// 导出
export const a = 1;
export const b = 2;
// 或
export default { a: 1, b: 2 };
// 导入
import { a, b } from './module.js';
import module from './module.js';
import * as module from './module.js';
2. 加载时机
CommonJS
// 运行时加载(动态加载)
const moduleName = process.env.NODE_ENV === 'production'
? './prod-module'
: './dev-module';
const module = require(moduleName); // ✅ 允许动态路径
// 循环引用示例
// a.js
console.log('a 开始');
exports.done = false;
const b = require('./b.js');
console.log('在 a 中,b.done = %j', b.done);
exports.done = true;
console.log('a 结束');
// b.js
console.log('b 开始');
exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %j', a.done);
exports.done = true;
console.log('b 结束');
ES6 模块
// 编译时加载(静态加载)
import module from './module.js'; // ✅ 必须静态路径
// ❌ 不允许动态路径(直接使用会报错)
const moduleName = './module.js';
import module from moduleName; // SyntaxError
// ✅ 使用动态导入(返回 Promise)
const moduleName = './module.js';
import(moduleName).then(module => {
// 使用模块
});
// 循环引用示例
// a.mjs
console.log('a 开始');
export const done = false;
import { done as bDone } from './b.mjs';
console.log('在 a 中,b.done = %j', bDone);
export const done = true;
console.log('a 结束');
// b.mjs
console.log('b 开始');
export const done = false;
import { done as aDone } from './a.mjs';
console.log('在 b 中,a.done = %j', aDone);
export const done = true;
console.log('b 结束');
3. 值类型差异
CommonJS - 值拷贝
// counter.js
let count = 0;
function increment() {
count++;
}
module.exports = { count, increment };
// main.js
const counter = require('./counter');
console.log(counter.count); // 0
counter.increment();
console.log(counter.count); // 0 ❌ 不改变
ES6 模块 - 值引用
// counter.mjs
export let count = 0;
export function increment() {
count++;
}
// main.mjs
import { count, increment } from './counter.mjs';
console.log(count); // 0
increment();
console.log(count); // 1 ✅ 实时更新
4. this 指向
// CommonJS
console.log(this); // {} 空对象
console.log(this === exports); // false
console.log(this === module.exports); // false
console.log(this === global); // false
// ES6 模块
console.log(this); // undefined
package.json 配置
{
"name": "my-app",
"version": "1.0.0",
"type": "module", // 或 "commonjs" 或不设置
"exports": {
".": {
"import": "./dist/esm/index.js", // ESM 入口
"require": "./dist/cjs/index.js" // CJS 入口
}
}
}
文件扩展名
// .js 文件(默认 CommonJS)
// 除非 package.json 有 "type": "module"
// .mjs 文件(总是 ESM)
import { something } from './module.mjs';
// .cjs 文件(总是 CommonJS)
const something = require('./module.cjs');
6. 互操作(互相导入)
ESM 导入 CJS
// cjs-module.js
module.exports = {
hello: 'world',
func: () => 'Hello'
};
// esm-module.mjs
import cjsModule from './cjs-module.js'; // ✅ 默认导入
console.log(cjsModule.hello); // 'world'
// 或使用 createRequire
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const cjsModule = require('./cjs-module.js');
CJS 导入 ESM(需要异步)
// esm-module.mjs
export const value = 42;
export default 'default export';
// cjs-module.js
// ❌ 不能直接 require
// const esm = require('./esm-module.mjs'); // Error
// ✅ 使用动态 import(返回 Promise)
(async () => {
const esm = await import('./esm-module.mjs');
console.log(esm.value); // 42
console.log(esm.default); // 'default export'
})();
CommonJS 缓存
// module.js
console.log('模块加载');
module.exports = { data: Math.random() };
// main.js
const m1 = require('./module'); // 输出: 模块加载
const m2 = require('./module'); // 无输出,使用缓存
console.log(m1 === m2); // true
console.log(m1.data === m2.data); // true
// 清除缓存
delete require.cache[require.resolve('./module')];
const m3 = require('./module'); // 再次输出: 模块加载
ES6 模块缓存
// module.mjs
console.log('ESM 模块加载');
export const data = Math.random();
// main.mjs
import * as m1 from './module.mjs'; // 输出: ESM 模块加载
import * as m2 from './module.mjs'; // 无输出,使用缓存
// 无法清除 ES6 模块缓存
8. 顶层 await
ES6 模块支持
// module.mjs
export const data = await fetchData();
// main.mjs
import { data } from './module.mjs';
console.log(data); // 直接使用,无需 async 函数
CommonJS 不支持
// ❌ 报错
const data = await fetchData();
module.exports = { data };
9. 目录结构示例
复制代码project/
├── package.json
├── src/
│ ├── cjs/
│ │ ├── index.js
│ │ └── utils.js
│ └── esm/
│ ├── index.mjs
│ └── utils.mjs
├── dist/
│ ├── cjs/
│ └── esm/
└── dual-package/
├── index.cjs # CommonJS 入口
├── index.mjs # ESM 入口
└── package.json
10. 性能差异
11. 最佳实践
双模式包配置
{
"name": "my-dual-package",
"version": "1.0.0",
"exports": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
},
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"files": ["dist"],
"scripts": {
"build:cjs": "tsc --module commonjs --outDir dist/cjs",
"build:esm": "tsc --module esnext --outDir dist/esm",
"build": "npm run build:cjs && npm run build:esm"
}
}
条件导入示例
// 检测环境
const isESM = typeof module === 'undefined' ||
(typeof process !== 'undefined' &&
process.versions?.node?.split('.')[0] >= '14');
// 条件导出
export async function loadModule() {
if (isESM) {
const { default: esmModule } = await import('./esm-module.mjs');
return esmModule;
} else {
const cjsModule = require('./cjs-module.js');
return cjsModule;
}
}
12. 迁移策略
// 步骤1:修改 package.json
{
"type": "module" // 或保持默认,使用 .mjs 扩展名
}
// 步骤2:重命名文件
// .js → .cjs (CommonJS 文件)
// .js → .mjs (ESM 文件)
// 步骤3:更新导入导出语法
// require() → import
// module.exports → export
// 步骤4:处理 __dirname 和 __filename
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
总结对比表
建议:新项目使用 ES6 模块,旧项目逐步迁移,库包提供双模式支持。