介绍
Node.js 自 v12 起就支持 ECMAScript 模块 (ESM),但生态系统仍然深深植根于 CommonJS (CJS)。将两个模块系统混合在一个项目中——或者从 ESM 代码中使用 CJS 包——会引入围绕导出、默认导入和全局的微妙陷阱。本文绘制了互操作边界并提供了平滑迁移的具体策略。
CJS 与 ESM 概览
| 方面 | 通用JS | 环境管理 |
|---|---|---|
| 文件扩展名 | .js、.cjs | .js、.mjs |
package.json 中的默认值 | 占位符_1 | 占位符_2 |
| 正在加载 | 同步 (require()) | 异步 (import) |
顶级 this | 占位符_1 | 占位符_2 |
__dirname / __filename | 可用 | 不可用 |
| 实时绑定 | 复制导出 | 实时绑定 |
配置模块系统
您可以通过 package.json "type" 字段控制模块系统:
{
"type": "module"
}
- 扩展名为
.mjs的文件始终被视为 ESM。 - 扩展名为
.cjs的文件始终被视为 CJS。 - 具有
.js扩展名的文件默认为"type"指定的内容。
从 ESM 导入 CJS
Node.js 允许 ESM 到 import CJS 模块。 CJS module.exports 映射到 ESM 导入的 默认导出:
// cjs-module.js
module.exports = { hello: 'world' };
// esm-module.mjs
import cjsModule from './cjs-module.js';
console.log(cjsModule.hello); // "world"
命名出口怪癖
CJS 的命名导出是通过 Node.js 中的静态分析自动检测的,但有局限性。动态或条件导出可能无法正确解析:
// This works only if Node can statically analyze the exports
import { hello } from './cjs-module.js';
如果遇到 export not found 错误,请回退到导入整个默认值:
import cjsModule from './cjs-module.js';
const { hello } = cjsModule;
从 CJS 导入 ESM
CJS 无法 require() ESM 模块。您必须使用 dynamic import(),它返回一个承诺:
// cjs-file.cjs
async function loadEsm() {
const esmModule = await import('./esm-module.mjs');
console.log(esmModule.namedExport);
}
当编写保留在 CJS 中但需要加载 ESM 插件的配置文件(例如 webpack、ESLint)时,这一点尤其重要。
__dirname ESM 中的替代方案
CJS 提供 __dirname 和 __filename 全局变量。 ESM 将它们替换为 import.meta:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
如需更简洁的方法,请使用 import.meta.resolve API(在旧 Node 版本中进行实验):
const resolvedPath = import.meta.resolve('./relative-path.js');
主要互操作陷阱
1.默认导出混乱
CJS 导出单个对象 - 不存在真正的“默认”与“命名”。当你写:
// CJS
exports.default = foo;
exports.named = bar;
ESM 将此解释为:
import { default as foo, named as bar } from './cjs.js';
但是 import cjs from './cjs.js' 绑定到 module.exports,这可能不是 CJS 作者的意图。
2.JSON模块
在 CJS 中,require('./data.json') 同步工作。在 ESM 中,您需要一个导入断言:
import data from './data.json' with { type: 'json' };
3. 顶级Await
ESM 支持顶级 await; CJS 没有。如果您需要在 CJS 上下文中使用顶级 await,请将代码包装在异步 IIFE 中。
迁移策略
| 步骤 | 行动 |
|---|---|
| 1 | 将 "type": "module" 添加到 package.json |
| 2 | 将文件重命名为 .mjs 或更新导入以使用完整扩展名 |
| 3 | 将 require() 替换为 import — 对 CJS 文件使用动态 import() |
| 4 | 将 __dirname / __filename 替换为 import.meta.url 模式 |
| 5 | 更新无法导入到 await import() 的任何依赖项 |
对于大型代码库,请考虑使用 双包 方法:使用 package.json 中的 "exports" 来传送 CJS 和 ESM 版本:
{
"exports": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
结论
ESM 是 Node.js 模块系统的未来,但从 CJS 的过渡需要仔细处理互操作边界。了解默认导出如何映射、何时使用动态 import() 以及如何用 import.meta.url 替换 CJS 全局变量将使您避免运行时意外。分阶段迁移与双包导出相结合,使您可以在不破坏现有消费者的情况下支持两个系统。

