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

