[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 數量隨著專案規模同步擴大,這樣會產生什麼隱憂?
- 請求過多
- 網站維護難處
- 變數全域污染造成功能受損
網站維護難處
根據瀏覽器的渲染機制,你會知道瀏覽器會 “由上而下” 依序讀取 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 [{},{}]
}
你會發現:(如下圖)
- 即便你在 f.js 沒有宣告此變數仍可讀取呼叫
- 你重複命名此變數名稱時會造成 shoppingCart 內的被覆蓋
這都是產生所謂變數全域污染。
//f.js
callItem() //[{},{}]
function callItem(){
return 'successful call!' //這樣就覆蓋掉 shoppingCart.js 的 test 函式定義
}
這個問題除了透過模組化外,也可以透過 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 會發出多次請求問題。