WEBKT

别再被模块报错折磨:Node.js 中 CommonJS 与 ESM 混用完全指南

9 0 0 0

在当前的 Node.js 生态中,我们正处于从 CommonJS(CJS)向 ES Modules(ESM)过渡的深水区。作为开发者,你一定遇到过这种心碎时刻:原本跑得好好的代码,引入一个新包后突然报出 ERR_REQUIRE_ESM;或者在 ESM 模块里想用 __dirname 却发现它是 undefined

虽然官方建议项目尽量保持模块风格统一,但在实际开发(尤其是维护老项目或引入纯 ESM 包)时,混用往往不可避免。理解 .cjs.mjspackage.json 的优先级,能帮你解决 90% 以上的模块兼容性报错。

1. 决定模块身份的三剑客

Node.js 判断一个文件是 CJS 还是 ESM,遵循以下优先级逻辑:

  1. 文件后缀名(最高优先级)
    • .mjs:始终被视为 ES Modules。
    • .cjs:始终被视为 CommonJS。
  2. package.json 中的 type 字段
    • "type": "module":该目录及子目录下所有 .js 文件都被视为 ESM。
    • "type": "commonjs"(或未设置):所有 .js 文件视为 CJS。
  3. 默认行为:如果既没有后缀区分,也没有 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-fetchchalk)时,必须通过这种异步方式引入。


4. 双模包(Dual Packages)的实现

如果你正在开发一个库,希望同时支持 requireimport,最规范的做法是在 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" 来简化开发体验。

码农老王 NodejsJavaScript后端开发

评论点评