今天想來看個基礎到不行,但之前一直沒有好好認識的 JavaScript 常識,也就是 JavaScript 的模組系統 - CommonJS 和 ES Module。
什麼是 JavaScript 的模組系統?
所謂的「JavaScript 模組系統」是指能將程式碼拆分為獨立、可重複使用之模組(module)的機制。透過模組系統,不同檔案之間可以用標準化的方式進行匯入與匯出,使專案在規模變大時仍然可以保持清晰的結構、良好的維護性與重複使用性。JavaScript 的模組系統也就是大家都有聽過的 CommonJS 和 ES Module。
JavaScript 模組系統的歷史背景
接下來讓我們快速來看一下關於 JavaScript 模組系統的歷史。
在 JavaScript 被創造出來的早期,它的用途主要是替網頁加入一些簡單的互動效果,例如按鈕點擊事件或表單驗證。當時的網頁互動仍以整頁重新載入為主,JavaScript 的功能範圍很小,也沒有跨檔案共享邏輯的需求,因此語言本身並沒有內建「模組」的概念。
隨著 Web 應用越來越複雜,JavaScript 在專案中的比重增加,程式也變得難以維護。前端普遍透過多個 <script> 依序載入 JavaScript 檔案,但這種方式缺乏標準化的依賴管理、容易造成全域變數污染,也很難維持清楚的架構。在這樣的背景之下,「讓 JavaScript 能模組化」逐漸成為前端與後端共同的需求。
雖然一直以來大家都知道 JavaScript 需要模組化的功能,但實際上真正促使模組系統出現的則是因為 Node.js 的出現。Node 需要在後端環境中管理大型程式,因此採用了 CommonJS(CJS)作為模組系統。但是 CommonJS 只存在於 Node.js,瀏覽器仍然缺乏官方的模組標準。為了讓 JavaScript 在前端、後端與瀏覽器生態中都有一致的模組規範,ECMAScript 在 2015 年正式引入了 ES Module(ESM)。
雖然 ES Module 已成為官方標準,但由於 JavaScript 生態中仍然存在大量使用 CommonJS 的套件,加上 ESM 在 Node.js 的支援是到後來才逐步成熟,因此 CommonJS 至今都還是有被使用。這也是為什麼到了現在,仍常常同時看到 CommonJS 與 ES Module 的原因。
JavaScript 的模組系統
CommonJS
CommonJS 是使用 require() 和 module.exports 進行匯入與匯出,由於 require() 是一個函式呼叫,所以模組間的相依關係是在執行階段(runtime)才會決定,也就是說必須等程式執行到那一行時,才能確定實際載入哪個模組,這個特性被稱作為「動態加載」。
因為相依關係不是在編譯階段就固定,打包工具 (bundler) 無法在打包時就完全確定哪些匯出有被使用、哪些從未被引用,因此 CommonJS 大多數時候無法進行精準的 Tree Shaking,只能採取保守策略將程式碼全部打包進去。
這裡也展示實際上使用 CommonJS 的寫法
假設有兩個 function。
function add(a, b) {
return a + b
}
function minus(a, b) {
return a - b
}匯出主要有兩個寫法,一個是匯出一個物件;一個是匯出單一的 function。
// 匯出方式1: 匯出一個物件
module.exports = {
add,
minus,
}
// 匯出方式2:單一匯出
module.exports = add匯入的寫法也會依照是匯出物件,還是單一匯出有差異。
// 對應「匯出物件」的匯入寫法
const { add, minus } = require('./functions')
// 對應:
// const add = require('./functions')// 對應「單一匯出」的匯入寫法:
const add = require('./functions')ES Module
ES Module 使用 import 和 export 進行匯入與匯出,這樣的語法能讓程式碼在尚未被執行前的解析階段就建立完整的相依關係圖,所以不需要像 CommonJS 一樣等到執行期(runtime)才能知道模組的相依關係,這種特性被稱為「靜態分析」。
因為 ES Module 的匯入與匯出都能在打包或編譯階段被靜態推導出來,所以打包工具(bundler)也就能較精準地判斷哪些匯出有被使用、哪些沒有被使用,因此能比 CommonJS 進行更有效且精準的 Tree Shaking。
這裡也展示實際上使用 ES Module 的寫法,一樣也準備兩個 function。
function add(a, b) {
return a + b
}
function minus(a, b) {
return a - b
}在 ES Module 這裡也有兩種匯出的方式,分別是 named export 和 default export。
named export 的寫法可以在宣告 function 時直接匯出
export function add(a, b) {
return a + b
}
export function minus(a, b) {
return a - b
}也可以先宣告 function 後,再透過物件的方式匯出
export { add, minus }想要匯入時,則都是透過以下的這個寫法匯入。
import { add, minus } from './functions.js'default export 的寫法一樣可以直接宣告就匯出
export default function add(a, b) {
return a + b
}或是先宣告再匯出
function add(a, b) {
return a + b
}
export default add在這個情況下,就會透過以下的寫法匯入。
import add from './functions.js'
JavaScript 如何決定使用哪一種模組
通常我們在專案中,會透過 package.json 的 type 來設定當前專案要使用哪個模組系統。如果是 "type": "commonjs" 就會使用 CommonJS,如果是 "type": "module" 則會使用 ES Module。如果沒有使用 package.json ,或是在 package.json 裡面沒有加上 type 的設定,就會使用預設的 CommonJS 。
若是不想要受到 package.json 的影響,也可以透過副檔名來強制使用其中一個模組系統。當副檔名為 .cjs 時會是使用 CommonJS,副檔名使用 .mjs 則會使用 ES Module。如果沒有特別設定這樣的副檔名,只是單純使用 .js 這個副檔名,就會依照 package.json 的設定決定使用那一個模組系統。
// package.json
{
"name": "my-app",
"type": "module",
"main": "index.js"
}總結
JavaScript 在最早的時候並沒有內建模組系統,所有 JavaScript 的程式都需要靠 <script> 載入,這也就導致很難針對 JavaScript 相關的程式碼進行依賴管理,也容易造成全域污染。但隨著前端邏輯變複雜,也就使得模組化需求越急迫。
一開始是後端的 Node.js 先使用了 CommonJS 作為模組系統,而瀏覽器直到 ES2015 才正式以 ES Module (ESM)作為標準的模組系統,因此在當前的生態中仍同時存在兩種模組系統。
雖然 CommonJS 和 ES Module 這兩者都是 JavaScript 的模組化系統,但兩者卻有差異:
-
CommonJS 是透過
require()、module.exports 的寫法匯入和匯出,且是於執行期(runtime)時進行動態載入。 -
ES Module 是透過
import、export的寫法匯入和匯出,且是在編譯期(build time) 才解析依賴進行靜態分析。
想要決定要使用哪一種模組化系統,可透過 package.json 的 "type" 或副檔名 .cjs / .mjs 來控制。
這次的主題就到這裡告一個段落,下一篇會延續模組系統延伸來看看 Tree Shaking。
