了解 JavaScript 模塊系統(tǒng)基礎(chǔ)知識(shí),搭建自己的庫(kù)

我想很多“前端工程師”都聽(tīng)過(guò)說(shuō)過(guò) “JavaScript 模塊”,那你們都知道如何處理它,以及它在日常工作中如何發(fā)揮作用嗎?
JS 模塊系統(tǒng)到底是什么呢
隨著 JavaScript 開(kāi)發(fā)越來(lái)越廣泛,命名空間和依賴(lài)項(xiàng)變得越來(lái)越難以處理,極客們?cè)缫呀?jīng)開(kāi)發(fā)出不同的模塊系統(tǒng)解決方案來(lái)解決該問(wèn)題。

為什么理解 JS 模塊系統(tǒng)很重要
我的日常工作是設(shè)計(jì)和項(xiàng)目架構(gòu),并且我很快意識(shí)到跨項(xiàng)目需要許多通用功能。我總是一次又一次地將這些功能復(fù)制粘貼到新項(xiàng)目中。
問(wèn)題是,每當(dāng)更改一部分代碼時(shí),我都需要在所有項(xiàng)目中手動(dòng)同步這些更改。為了避免所有這些繁瑣的手動(dòng)任務(wù),我決定提取通用功能并從中組成一個(gè) NPM 軟件包。這樣,團(tuán)隊(duì)中的其他人將能夠?qū)⑺鼈冎匦掠米饕蕾?lài)項(xiàng),并在每次推出新版本時(shí)都可以對(duì)其進(jìn)行更新。
這種方法具有一些優(yōu)點(diǎn):
如果核心庫(kù)中有一些更改,則只需在一個(gè)地方進(jìn)行更改,而無(wú)需為同一件事重構(gòu)所有應(yīng)用程序的代碼。 所有應(yīng)用程序保持同步。無(wú)論何時(shí)進(jìn)行更改,所有應(yīng)用程序僅需要運(yùn)行 npm update?命令。

庫(kù)的源碼
因此,下一步是發(fā)布庫(kù)。
這是最困難的部分,因?yàn)槲夷X海中突然跳出一堆東西,例如:
如何使用搖樹(shù)優(yōu)化 應(yīng)該針對(duì)哪些 JS 模塊系統(tǒng)(CommonJS、AMD、ES modules) 需要轉(zhuǎn)譯源碼嗎 需要打包源碼嗎 應(yīng)該發(fā)布哪些文件
在發(fā)布第三方庫(kù)(組件庫(kù),工具庫(kù))時(shí),我們每個(gè)人的腦海中都應(yīng)該冒出這些問(wèn)題。
來(lái), 我們一步步解決以上的問(wèn)題。
不同類(lèi)型的 JS 模塊系統(tǒng)
1. CommonJS
由?Node.js 實(shí)現(xiàn) 多用在服務(wù)器端安裝模塊時(shí) 沒(méi)有 runtime/async 模塊 通過(guò) require?導(dǎo)入模塊通過(guò) module.exports?導(dǎo)出模塊無(wú)法使用搖樹(shù)優(yōu)化,因?yàn)楫?dāng)你導(dǎo)入時(shí)會(huì)得到一個(gè)模塊時(shí),得到的是一個(gè)對(duì)象,所以屬性查找在運(yùn)行時(shí)進(jìn)行,無(wú)法靜態(tài)分析 會(huì)得到一個(gè)對(duì)象的副本,因此模塊本身不會(huì)實(shí)時(shí)更改 循環(huán)依賴(lài)的不能優(yōu)雅處理 語(yǔ)法簡(jiǎn)單
2. AMD 異步模塊定義
由 ?RequireJs ?實(shí)現(xiàn) 當(dāng)你在客戶端(瀏覽器)環(huán)境中,異步加載模塊時(shí)使用 通過(guò) require?實(shí)現(xiàn)導(dǎo)入語(yǔ)法復(fù)雜
3. UMD 通用模塊定義
CommonJs + AMD ?的組合(即 CommonJs 的語(yǔ)法 + AMD 的異步加載) 可以用于 AMD/CommonJs 環(huán)境。 UMD 還支持全局變量定義,因此,UMD 模塊能夠在客戶端和服務(wù)器上工作。
4. ES modules
用于服務(wù)器/客戶端 支持模塊的 Runtime/static loading 當(dāng)你導(dǎo)入時(shí),獲得是實(shí)際對(duì)象 通過(guò) import?導(dǎo)入,通過(guò)export?導(dǎo)出靜態(tài)分析——你可以決定編譯時(shí)的導(dǎo)入和導(dǎo)出(靜態(tài)),你只需要看源碼,不需要執(zhí)行它 由于 ES6 支持靜態(tài)分析,因此搖樹(shù)優(yōu)化是可行的 始終獲取實(shí)際值,以便實(shí)時(shí)更改模塊本身 比 CommonJS 有更好的循環(huán)依賴(lài)管理
現(xiàn)在,我們了解了不同類(lèi)型的 JS 模塊系統(tǒng)以及它們?nèi)绾窝葑儭?/p>
盡管所有工具和現(xiàn)代瀏覽器都支持?ES modules,但我們?cè)诎l(fā)布庫(kù)時(shí)不知道用戶如何利用我們的庫(kù)。因此,我們必須確保我們的庫(kù)在所有環(huán)境中都能正常工作。
讓我們深入研究并設(shè)計(jì)一個(gè)示例庫(kù),更好地回答與發(fā)布庫(kù)有關(guān)的所有問(wèn)題。
我已經(jīng)建立了一個(gè)小型的 UI 庫(kù)(你可以在?GitHub 上找到源代碼),并且我將分享我在編譯,打包和發(fā)布中的所有經(jīng)驗(yàn)和探索。

目錄結(jié)構(gòu)
在這里,我們有一個(gè)小的 UI 庫(kù),其中包含 3 個(gè)組件:Button,Card 和 NavBar。讓我們一步步進(jìn)行編譯并發(fā)布。
發(fā)布前的最佳實(shí)踐
1. 搖樹(shù)優(yōu)化(Tree Shaking)
webpack 官方文檔有說(shuō)明
搖樹(shù)優(yōu)化是一個(gè)術(shù)語(yǔ),通常用于描述移除 JavaScript 上下文中的未引用代碼(dead-code)。它依賴(lài)于 ES2015 模塊系統(tǒng)中的靜態(tài)結(jié)構(gòu)特性,例如 [import][4]?和[export][5]。這個(gè)術(shù)語(yǔ)和概念實(shí)際上是興起于 ES2015 模塊打包工具 rollup。新的 webpack 4 正式版本,擴(kuò)展了這個(gè)檢測(cè)能力,通過(guò)package.json?的"sideEffects"?屬性作為標(biāo)記,向 compiler 提供提示,表明項(xiàng)目中的哪些文件是純的 ES2015 模塊,由此可以安全地刪除文件中未使用的部分。webpack 和 Rollup 都支持搖樹(shù)優(yōu)化,這意味著我們需要牢記某些事情,以便我們的代碼可被 Tree Shaking。
2. 發(fā)布所有模塊形態(tài)
我們應(yīng)該發(fā)布所有模塊形態(tài),例如 UMD 和?ES Module,因?yàn)槲覀冇肋h(yuǎn)不知道用戶在哪個(gè)版本的瀏覽器或 webpack 中使用此庫(kù)/包。 即使所有打包程序(如 ?webpack ?和 ?Rollup)都能解析 ES Module ,但如果我們的使用者使用的是 webpack 1.x,則它無(wú)法解析 ES 模塊。
//?package.json
{
??"name":?"js-module-system",
??"version":?"0.0.1",
package.json 文件的 main 字段通常用于指向?UMD 版本的庫(kù)/包。 package.jso 文件的 module?字段用于指向?ES?版本的庫(kù)/包。
鮮為人知的事實(shí):webpack 使用 resolve.mainfields 確定檢查
package.json?中的哪些字段。
性能提示:由于所有現(xiàn)代瀏覽器現(xiàn)在都支持 ES 模塊,因此也請(qǐng)務(wù)必發(fā)布 ?ES 版本的庫(kù)/包。這樣一來(lái),可以減少編譯次數(shù),最終可以減少向用戶交付的代碼。這將提高應(yīng)用程序的性能。
那么,下一步是什么?編譯還是打包?我們應(yīng)該使用什么工具?啊,這是最棘手的部分!讓我們深入研究研究。
webpack vs Rollup vs Babel
這些我們?cè)谌粘9ぷ髦惺褂玫墓ぞ撸糜诔休d我們的應(yīng)用程序/庫(kù)/軟件包。沒(méi)有它們,我無(wú)法想象現(xiàn)代的 Web 開(kāi)發(fā)有多么糟糕。因此,我們無(wú)法將它們進(jìn)行比較 ?
每種工具都有其自身的優(yōu)勢(shì),并根據(jù)使用者的需求達(dá)到不同的目的。
現(xiàn)在讓我們看一下這些工具:
webpack
webpack?是一個(gè)很棒的模塊打包工具, 它被廣泛接受并且主要用于構(gòu)建 SPA。它提供了開(kāi)箱即用的所有功能,例如代碼拆分、按需加載、搖樹(shù)優(yōu)化等,并且它本身使用的是 CommonJS 模塊系統(tǒng)。
RollupJS
RollupJS 還是類(lèi)似于 webpack 的模塊打包器。但是,RollupJS 的主要優(yōu)點(diǎn)是它遵循 ES6 修訂版中包含的代碼模塊的新標(biāo)準(zhǔn)化格式,因此你可以使用它來(lái)打包??ES module variant 的 library/package,但它不支持按需加載。
Babel
Babel 是 JavaScript 的編譯器,以將 ES6 代碼轉(zhuǎn)換為可在你的瀏覽器(或服務(wù)器)中運(yùn)行的代碼而聞名。請(qǐng)記住,它只是編譯而不會(huì)打包你的代碼。
我的建議:對(duì)庫(kù)使用 Rollup.js,對(duì)應(yīng)用程序使用 webpack。
編譯(Babel-ify)源代碼還是直接打包源代碼
在構(gòu)建我的 NPM 庫(kù)時(shí),我花費(fèi)了大量時(shí)間來(lái)試圖找出該問(wèn)題(如何編譯、如何打包)的答案。我開(kāi)始挖掘自己的 node_modules,查找所有優(yōu)秀的庫(kù)并檢查它們的構(gòu)建系統(tǒng)。

對(duì)比 libraries/packages 構(gòu)建的輸出
在查看了不同 libraries/packages 的構(gòu)建輸出之后,我清楚地了解了這些庫(kù)的作者在發(fā)布之前可能會(huì)想到的不同策略。以下是我的觀察。
如你在上圖中所看到的,我已根據(jù)它們的特性將這些庫(kù)/軟件包分為兩組:
UI Libraries-UI 庫(kù)( styled-components,material-ui)Core Packages-核心包( react,react-dom)
你可能已經(jīng)弄清楚了這兩組之間的區(qū)別。
UI Libraries
有一個(gè) dist 文件夾,該文件夾是針對(duì) ES 和 UMD/CJS 模塊系統(tǒng) 的打包和壓縮版本。 有一個(gè)?lib 文件夾,用來(lái)存放被編譯后的代碼。
Core Packages
只有一個(gè)文件夾,其中包含針對(duì) CJS 或 UMD 模塊系統(tǒng)的打包和壓縮版本。
但是,為什么 UI Libraries 和 Core Packages 的構(gòu)建輸出有所不同?
UI Libraries
想象一下,如果我們只是發(fā)布庫(kù)的 bundled version 將其托管在 CDN 上,我們的用戶將直接在
