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 全域變數將使您避免執行時間意外。分階段遷移與雙包導出相結合,使您能夠在不破壞現有消費者的情況下支援這兩個系統。