两套并存
Node 同时支持:
| CommonJS(CJS) | ES Module(ESM) | |
|---|---|---|
| 关键词 | require / module.exports |
import / export |
| 文件后缀 | .cjs 或 .js(默认) |
.mjs 或 .js(需 "type": "module") |
| 同步加载 | ✓ | ✗(异步) |
| 顶层 await | ✗ | ✓ |
__dirname / __filename |
自动有 | 需要 import.meta.url 换算 |
| 现代推荐 | 老项目 / 工具 | 新项目首选 |
CommonJS(传统)
// math.js
function add(a, b) { return a + b; }
const PI = 3.14;
module.exports = { add, PI };
// 或单个:module.exports = function () { ... };
// index.js
const { add, PI } = require('./math');
console.log(add(1, 2), PI);
ESM(现代)
package.json 里:
{ "type": "module" }
// math.js
export function add(a, b) { return a + b; }
export const PI = 3.14;
// 或默认导出:
export default function () { ... };
// index.js
import { add, PI } from './math.js'; // ★ 注意必须带 .js 扩展名
import myDefault from './math.js';
console.log(add(1, 2), PI);
顶层 await(ESM 独有)
// 直接在文件顶层用 await
const config = await loadConfig();
console.log(config);
CJS 不能这么写。
ESM 里的 __dirname / __filename
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Node 20.11+ 有更简单的:
const __dirname = import.meta.dirname;
const __filename = import.meta.filename;
互操作(混用)
ESM 里 require CJS 包
ESM 可以 import 大多数 CJS 包:
import express from 'express'; // express 是 CJS 包,但能 import
如果不行(命名导出问题):
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const someCjsLib = require('some-cjs-lib');
CJS 里 require ESM 包
不能直接 require——CJS 是同步,ESM 是异步:
// ❌ 报错
const lib = require('esm-only-package');
// ✓ 用动态 import(返回 Promise)
const lib = await import('esm-only-package');
选哪个
新项目:
- 库 / 工具 / Web 服务 → ESM
- 跑构建脚本 / 简单 CLI → CJS 也 OK
老项目:
- 维护就维护,别为切而切
- 想升级 → 一步步迁(先小模块)
实战:迁 CJS 到 ESM
# 1. package.json 加 "type": "module"
# 2. require → import
# CJS:
const fs = require('fs');
# ESM:
import fs from 'fs';
# 或:
import { readFileSync } from 'fs';
# 3. module.exports → export
# CJS:
module.exports = { foo };
module.exports.bar = bar;
# ESM:
export const foo = ...;
export const bar = ...;
export default ...;
# 4. 所有相对路径 import 加 .js 后缀
import './utils'; ❌
import './utils.js'; ✓
# 5. __dirname / __filename 用上面提到的换算
坑
- ESM 必须带文件后缀——
import './utils'报错,要'./utils.js' - JSON 导入:
import data from './data.json'在新 Node 要with { type: 'json' } - 循环依赖在 ESM 行为不同——避免循环引用是好做法
- CJS 包的 default export 在 ESM 里不一定能直接解构——看包的 exports 字段
下一篇:JS 数据类型。