前端P.11:模組 CommonJS ESM
🥜 前情提要
在上一回中,我們提到使用別人的模組亂操作的下場,所以使用閉包來避免讓別人亂操作,但是模組這東西在JavaScript又是個災難。今天就來解析吧。
模組:
因為在ES6之前還沒有模組化標準給大家用,在ES6之前都是如果你include兩份JS模組:
- 第一份為SayHello.js,在window上宣告了temp
- 第二份為SayHell.js,在window上宣告了temp
而今天想要使用SayHello
的某個功能
1 | function hello (){ |
temp.hello
預計會是Hello,但是你不知道第二份模組物件名稱衝突了,於是就使用了SayHell
的同名功能
1 | function hello (){ |
於是變成Hell。
所以呢在ES6之前熱心的JavaScript社群們提出一些模組化加載方案,主要可以分為兩派:CommonJS,而ES6之後推出官方標準,所以我們JavaScript當前在加載模組時候有兩種標準(1個官方、1個野雞自幹派)
- CommonJS
- ESM
模組化的歷史:
大概在2009年當時有個社群叫做CommonJS,該社群有很多大佬,這群大佬推出了 ServerJS 的規範,用於處理模塊加載,這個規範在NodeJS上實踐了並且效果不錯,於是這群大佬又推出了新版本,並且要讓這規範能夠支援瀏覽器,所以把 ServerJS 改名為 CommonJS,但這次新版本不是那麼順利,內部爭議不斷於是分歧就產生了。
分歧產生了新流派,這個新流派提議,當前瀏覽器特徵不應該直接使用CommonJS規範,並提出自己的規範,但是該新規範被受社群內部爭議,於是這個流派被CommonJS社群獨立出去了,該流派的人把新規範取名為AMD並且自創了一個同名AMD社群,就是第二個流派的規範,他們的runtime 是 RequireJS。
分歧產生了 Modules/2.0 流派,這個流派跟AMD那夥人差不多,但是應該盡可能與舊版規範保持一致。所以該派也獨立了,成立CMD規範,但很可惜當時 RequireJS 非常火,被打敗了。其中該流派的創始人把GitHub和官網清空,只留下一句話「我會回來的,帶著更好的東西」,就此消失。這句話給一個工程師有很大的影響,於是那個人將CMD實做在seaJS。
上述歷史中總共有三個野雞流派,但是只有一人生存至今,剩餘的流派都被歷史淘汰了。
- CommonJS負責Sever的NodeJS部分
- 而後續官方推出的ES6模塊標準則負責瀏覽器端。
故以下我們用這兩者進行比對和範例。
靜態優化:
首先CommonJS 在加載模組時候都只能在運行時才能確定這些東西,舉個範例:
1 | let { stat, exists, readFile } = require('./fs'); |
這種加載方式會有個問題,只有運行到時候才能得到這個物件,導致沒辦法在編譯階段進行優化,簡稱無法進行靜態優化。
那麼ES6官方版的模組化加載方案是如何做到:
1 | 這是ES6官方版本的模塊加載方案: |
上面的程式碼是直接去fs模組加載這三個函數,而其他沒指定的函數就不會進行加載,這種方式可以實現在編譯時後就進行檢查,就導致模組加載的效率會比CommonJS快,因為不是執行到那行時候才加載。
CommonJS 模組標準:
該規範主要運用於服務器端,也就是NodeJS,他新增了一個函數叫做:「require」 以及 「exports」這個物件,對應了導入和導出功能。
1 | // Math.js |
1 | // Main.js |
而以上就是CommonJS這個規範所制定的模組加載部分,而規範就是規範,還是要有人實做,於是NodeJS跳出來說我將實現這個規範,於是我們可以在NodeJS這個runtime實現該規範的加載方案。
快取緩存值的問題:
1 | // lib.js |
1 | var mod = require('./lib'); |
理論上應該要是被累加輸出成4,但是因為他是在輸出時就把value快取了,mod.counter在一開始require時後就被快取緩存下來了,所以不受到內部影響,因為這邊是獲取緩存的結果,這是因為這是個值而不是函數,所以被快取下來。
1 | // lib.js |
1 | var mod = require('./lib'); |
我們將 mod.counter 對外導出,並且是個存取子是個函數,無法被快取,就可以獲得正確被累加的結果。
CommonJS如何實現在瀏覽器端?
由上述歷史和例子我們可以知道,今天如果你專案採用CommonJS來加載模組,那麼你的runtime不就只能在NodeJS嗎? 瀏覽器只認得ES標準,誰會處理CommonJS這個野雞派的規範,於是有人利用NodeJS開發一個工具叫做 Babel 同名於 聖經中的巴別塔,其用法後續會介紹。
利用Babel 可以把CommonJS的模塊加載改成瀏覽器認得的 ES6模塊加載規則,而Babel 功能不只如此,不然怎麼敢跟巴別塔同名的膽子,他還可以把高版本的ES轉成低版本的ES,也就是我不用管瀏覽器支援哪個版本,我一慮用ES10爽爽寫,Babel 會幫我翻譯成瀏覽器能認得的ES版本。
ES6 模組標準:
該規範主要運用於瀏覽器端,他新增了兩個函數叫做:「import」 以及 「export」,對應了導入和導出功能。
export:
1 | // profile.js |
以上寫法等價於下方寫法:
1 | // profile.js |
以上寫法等價於下方寫法:
1 | // profile.js |
第四種 export default 寫法:
1 | // profile.js |
以上就是export 的寫法,而ES6 export 不像是 CommonJS 的 exports 會有快取緩存問題
1 | export var foo = 'bar'; |
import:
1 | // main.js 對應於第一、二、三種export方法 |
其中import的變數名稱必須跟export一模一樣,如果只想導入其中一個並且想改名可以使用以下方法:
1 | // main.js 對應於第一、二、三種export方法 |
而如果想import所有項目的話可以使用以下方法:
1 | // main.js |
而第四種export 方式 可以使用以下import 方式使用:
1 | // main.js |
ES6模組標準如何實現在NodeJS?
NodeJS改版後也開始支援ES6模塊語法,但是原本NodeJS就有自己的模塊標準 CommonJS ,現在還要相容另一種 ES6模塊標準,所以NodeJS要求
在package.json 未開啟type或 type為commonjs的情況,採用ES6標準模組的檔案必須命名為 .mjs ,並且會自動開啟嚴格模式。
在package.json 未開啟type或 type為commonjs的情況,採用CommonJS的檔案必須命名為 .js。
在package.json 開啟type為module的情況,採用ES6標準模組的檔案必須命名為 .js。
在package.json 開啟type為module的情況,採用CommonJS的檔案必須命名為 .cjs。
這樣NodeJS就會採取不同的模組標準去解釋該文件,而 commonjs 和 ES6混用 為不推薦做法,因為兩者在靜態優化的差異,可能會有BUG
或許還要經過幾年NodeJS才會完全捨棄 CommonJS吧,畢竟現在人要學兩種導入導出方式,真的好麻煩。