Featured image of post Node.js 中的 ESM 与 CommonJS 互操作挑战Featured image of post Node.js 中的 ESM 与 CommonJS 互操作挑战

Node.js 中的 ESM 与 CommonJS 互操作挑战

了解 ESM / CJS 模块边界互操作,了解路径、导出语法和 import.meta 差异。

介绍

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 或更新导入以使用完整扩展名
3require() 替换为 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 全局变量将使您避免运行时意外。分阶段迁移与双包导出相结合,使您可以在不破坏现有消费者的情况下支持两个系统。