Skip to main content

[JS] 模組化筆記

在沒有模組的時代

試想,今天你接手到一個不支援模組語法 require,import 的舊專案,你會怎麼追加後續前端功能?若是透過 script 來撰寫前端功能邏輯,程式碼架構會如下:

<html>
<head>....</head>
<body></body>
<script src="/js/a.js"></script>
<script src="/js/b.js"></script>
<script src="/js/c.js"></script>
<script src="/js/login.js"></script>
<script src="/js/shoppingCart.js"></script>
<script src="/js/f.js"></script>
....
</html>

每個 script 代表網站內的一項功能(例如:login.js, shoppingCart.js),可以預想 script 數量隨著專案規模同步擴大,這樣會產生什麼隱憂?

  1. 請求過多
  2. 網站維護難處
  3. 變數全域污染造成功能受損

網站維護難處

根據瀏覽器的渲染機制,你會知道瀏覽器會 “由上而下” 依序讀取 script,所以 script 正確的排序非常重要,假設你今天專案有在 shoppingCart 內使用 lodash 的套件功能(如下圖),若要功能順利運行,勢必要先載入 lodash 再來是 shoppingCart,否則會有功能故障,隨著專案擴大、引入套件功能繁瑣,你會越來越難保各個 script 之間是否有耦合問題。

<html>
<head>....</head>
<body></body>
<script src="/js/a.js"></script>
<script src="/js/b.js"></script>
<script src="/js/c.js"></script>
<script src="/js/shoppingCart.js"></script>
<script src="/node_module/lodash.js"></script>
<script src="/js/f.js"></script>
....
</html>

變數全域污染造成功能受損

通常,不支援 require, import 語法的專案,是基於舊版 javascript ( ES5 甚至更早),所以宣告變數只能使用 var, var 宣告的變數會直接被掛載到 window 物件上。所以當你在 shoppingCart.js 內宣告了一個函式 callItem

//shoppingCart.js

function callItem(){
return [{},{}]
}

你會發現:(如下圖)

  1. 即便你在 f.js 沒有宣告此變數仍可讀取呼叫
  2. 你重複命名此變數名稱時會造成 shoppingCart 內的被覆蓋

這都是產生所謂變數全域污染。

//f.js

callItem() //[{},{}]

function callItem(){
return 'successful call!' //這樣就覆蓋掉 shoppingCart.js 的 test 函式定義
}
tip

這個問題除了透過模組化外,也可以透過 babel 等第三方套件解決。

模組化怎麼解決了這些問題

在這邊先以 ESM (ES Module) 為例

變數全域污染

ESM 系統下,模組內定義的變數、函式不會提升(hoisting)到全域命名空間,而是在各自的模組空間內。延續上面的例子,我們將 shoppingCart.js 腳本模組化

<script src="/js/shoppingCart.js" type="module"></script>

再同樣試著在 f.js 內呼叫 callItem 函式,會發現:

Uncaught ReferenceError: test is not defined
at f.js:3:1

此時,因為 callItem 已經沒有提升至全域命名,所以 f.js 無法存取。那如果要跨模組使用函式、變數怎麼做呢?這就要說明 ESM 的輸出 export、輸入語法 import

//shoppingCart.js

// 輸出此方法
export function callItem(){
return [{},{}]
}

在 f.js 內則使用 import 引入後,就能重複使用這個函式。

import {callItem} from "/js/shoppingCart.js"

callItem()

網站維護難處

如上面,沒有模組化的情境下,每個功能都需要透過 script 去進行加載,但當你改用 ESM 模組系統後,你可以改為將功能統一引入一個主要的模組(如下圖)

//main.js

import {aFunc} from "/js/a.js"
import {bFunc} from "/js/b.js"
import {cFunc} from "/js/c.js"
import {loginFunc} from "/js/login.js"
import {callTtem} from "/js/shoppingCart.js"

這樣你就只需要將 main.js 這個腳本進行加載

<html>
<head>....</head>
<body></body>
<script src="/js/main.js" type="module"></script>
</html>

一次解決了需要手動確認 script 順序避免功能問題、以及過多 script 會發出多次請求問題。