本篇重點
- 解析 CommonJS(CJS)與 ES Modules(ESM)模組語法差異
- CJS 使用
require()與module.exports,ESM 使用import與export - CJS 採同步載入,ESM 採靜態分析與非同步初始化
- ESM 天生支援 Tree Shaking,利於效能優化
- 整理專案 CJS 到 ESM 的轉換,通常需要調整載入、輸出、路徑、副檔名、全域變數等寫法
早期 Node.js 主要使用 CommonJS(CJS)作為模組系統,其設計目標偏向伺服器端同步執行環境。
後來 JavaScript 官方推出 ES Modules(ESM),作為語言標準模組系統。瀏覽器、Node.js、打包工具與現代前端框架逐漸全面採用 ESM。
CJS 與 ESM 的語法差異
CJS:require() 和 exports
CJS 使用 require() 載入模組,並透過 module.exports 或 exports 輸出內容。
範例
1 | function add(a, b) { |
1 | // 整包引入成一個物件 |
ESM:import 和 export
ESM 使用 import 載入模組,並透過 export (Named Exports) 或是 export default (Default Export) 輸出內容。
範例
一個檔案中只能有一個 export default,但可以同時擁有多個具名匯出(Named exports,export)
1 | // 1. 預設匯出 (只能有一個) -> 主角 |
1 | // 主角不用大括號,配角們要放進大括號裡 |
加載機制差異
CJS:同步載入
Node.js 會在程式碼執行到 require() 時才進行讀取與解析,這種方式的優點是簡單直接並且可以動態載入,但缺點是:
- 無法在編譯階段分析依賴
- 動態特性也會增加分析難度
- 不利於最佳化
- 不適合瀏覽器環境
範例
1 | const moduleA = require('./moduleA'); |
ESM:靜態分析與非同步初始化
import 必須在檔案頂層,Node.js 會在程式執行前先建立依賴圖(Dependency Graph),再進行模組初始化。
- 可提前分析依賴
- 可進行最佳化
- 可支援 Tree Shaking
- 更適合瀏覽器與現代打包工具
範例
1 | import path from 'path'; |
副檔名與專案設定
Node.js 必須明確知道檔案使用 CJS 還是 ESM,預設將 .js 檔案視為 CJS 模組。若要啟用 ESM,需透過更改副檔名或修改專案的 package.json 設定來實現切換。
副檔名區分:強制指定
.mjs代表 ESM,.cjs代表 CJS。專案級別設定:在
package.json中加入"type": "module",此後專案內所有的.js檔案都會被解析為 ESM。package.json 1
2
3
4
5
6{
"name": "my-project",
"version": "1.0.0",
"type": "module", // 此設定會讓所有的 .js 檔案都會被解析為 ESM
"main": "app.js"
}
Tree Shaking
Tree Shaking 是現代打包工具的重要最佳化機制,一種 Dead Code Elimination 技術,主要用於移除程式碼中未被實際呼叫的程式碼。
範例
CJS 的 require() 為動態執行,通常無法安全分析實際使用內容,因此 Tree Shaking 能力較差。
ESM 天生支援靜態分析,因此工具可以知道哪些 export 未被使用。
1 | export function subtract(a, b) { |
1 | import { subtract, PI } from './math.js'; |
__dirname 與 __filename 差異
在 CJS 中,Node.js 會自動注入 __dirname (當前目錄路徑) 與 __filename (當前檔案路徑) 等全域變數。
例如
1 | const path = require('path'); |
在 ESM 中不提供這些全域變數,需引用模組來取得路徑。
範例
1 | import { fileURLToPath } from 'node:url'; |
互操作性
在過渡時期,專案中經常會遇到 CJS 與 ESM 混用的情況,這會帶來一些限制與痛點:
- ESM 載入 CJS:可以直接使用
import載入 CJS 模組,但通常只能取得 Default Export 的形式,無法依賴 Named Exports 的靜態分析。 - CJS 載入 ESM:無法使用同步的
require()載入 ESM 模組,這會引發ERR_REQUIRE_ESM錯誤。必須使用非同步的import()函式進行動態載入。
從 CJS 轉換至 ESM

- 更新 package.json
在package.json加入"type": "module"設定,宣告專案全面採用 ESM 規範。 - 重構載入與輸出語法
全面檢視檔案,將所有的require()替換為import,並將module.exports與exports替換為export/export default。 - 補齊本地檔案的副檔名
在 ESM 中,匯入本地端相對路徑的檔案時,必須寫出完整的副檔名(例如:import { helper } from './utils.js')。不寫副檔名或依賴index.js省略寫法的機制在 ESM 中預設不支援。 - 替換全域變數
找出專案中使用的__dirname、__filename以及require.resolve等 CJS 專有變數,使用import.meta.url配合fileURLToPath進行替換。 - 處理第三方依賴套件
檢查node_modules中是否有不相容的舊套件。 - 修正 JSON 檔案載入
在 ESM 中不能直接import data from './data.json',必須加上 Import Assertions (或 Import Attributes) 語法,或是改用fs.readFileSync讀取。
結論
CJS 與 ESM 最大差異在於:
- 模組載入方式
- 靜態分析能力
- Node.js 執行模型
CJS 偏向 Node.js 傳統伺服器架構,ESM 則是現代 JavaScript 生態。
ESM 帶來:
- Tree Shaking
- Top-level await
- 更佳的依賴分析
- 更一致的跨平台模組標準



