别再被模块报错折磨:Node.js 中 CommonJS 与 ESM 混用完全指南
在当前的 Node.js 生态中,我们正处于从 CommonJS(CJS)向 ES Modules(ESM)过渡的深水区。作为开发者,你一定遇到过这种心碎时刻:原本跑得好好的代码,引入一个新包后突然报出 ERR_REQUIRE_ESM;或者在 ESM 模块里想用 __dirname 却发现它是 undefined。
虽然官方建议项目尽量保持模块风格统一,但在实际开发(尤其是维护老项目或引入纯 ESM 包)时,混用往往不可避免。理解 .cjs、.mjs 和 package.json 的优先级,能帮你解决 90% 以上的模块兼容性报错。
1. 决定模块身份的三剑客
Node.js 判断一个文件是 CJS 还是 ESM,遵循以下优先级逻辑:
- 文件后缀名(最高优先级):
.mjs:始终被视为 ES Modules。.cjs:始终被视为 CommonJS。
- package.json 中的
type字段:"type": "module":该目录及子目录下所有.js文件都被视为 ESM。"type": "commonjs"(或未设置):所有.js文件视为 CJS。
- 默认行为:如果既没有后缀区分,也没有
type配置,Node.js 默认将其视为 CJS。
技巧: 如果你的项目是 ESM 类型,但由于某些工具(如 Webpack 配置、数据库迁移脚本)必须使用 CJS,请直接将该脚本后缀改为 .cjs。
2. 在 ESM 中加载 CommonJS
这是最常见的场景,通常比较简单。在 .mjs 或 "type": "module" 的环境下:
// index.mjs
import lodash from 'lodash'; // 默认导入
import { readFileSync } from 'fs'; // 命名导入(Node.js 内置模块支持)
import pkg from './config.cjs'; // 导入自定义的 CJS 文件
注意事项:
- 静态分析限制:Node.js 能够通过静态分析识别 CJS 的
module.exports。但如果 CJS 文件导出的是复杂的动态对象,解构赋值可能会失败,此时建议使用默认导入后再手动解构。 - 没有变量:在 ESM 中,
__dirname和__filename是不存在的。你需要通过以下方式获取:import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename);
3. 在 CommonJS 中加载 ESM(硬核部分)
这是报错的重灾区。由于 CJS 是同步加载的,而 ESM 是异步加载的,你不能在 CJS 中使用 require() 来加载一个 ESM 模块。
错误示范:
const myEsmLib = require('./module.mjs'); // 抛出 ERR_REQUIRE_ESM 错误
正确姿势:使用动态 import()
动态 import() 是异步的,它返回一个 Promise。这是 CJS 加载 ESM 的唯一途径。
// server.cjs
async function start() {
const { someFunction } = await import('./modern-tool.mjs');
someFunction();
}
start();
应用场景: 当你想在老项目中使用只提供 ESM 版本的第三方库(如新版的 node-fetch 或 chalk)时,必须通过这种异步方式引入。
4. 双模包(Dual Packages)的实现
如果你正在开发一个库,希望同时支持 require 和 import,最规范的做法是在 package.json 中配置 exports:
{
"name": "my-library",
"exports": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
这样,当用户 import 时 Node 会指向 .mjs,而 require 时指向 .cjs。
5. 常见坑点总结
| 特性 | CommonJS (.cjs) |
ES Modules (.mjs) |
|---|---|---|
| 加载方式 | require() |
import / export |
| 加载性质 | 同步 | 异步 |
| 顶级 Await | 不支持 | 支持 (Top-level await) |
| 全局变量 | __dirname, module, exports |
import.meta.url |
| 严格模式 | 需手动开启 "use strict" |
默认强制开启 |
结语
在 Node.js 中混合使用模块系统虽然显得有些琐碎,但只要抓住 “后缀名决定论” 和 “CJS 必须动态 import ESM” 这两条铁律,大部分报错都能迎刃而解。对于新项目,强烈建议全面拥抱 ESM,通过 package.json 的 "type": "module" 来简化开发体验。