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

我想很多“前端工程師”都聽(tīng)過(guò)說(shuō)過(guò) “JavaScript 模塊”,那你們都知道如何處理它,以及它在日常工作中如何發(fā)揮作用嗎?
JS 模塊系統(tǒng)到底是什么呢
隨著 JavaScript 開(kāi)發(fā)越來(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ū)⑺鼈冎匦掠米饕蕾図?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)依賴的不能優(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)依賴管理
現(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)。它依賴于 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ぷ髦惺褂玫墓ぞ?,用于承載我們的應(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 上,我們的用戶將直接在 標(biāo)記中使用它?,F(xiàn)在,如果使用者只想使用 組件,則他們必須加載整個(gè)庫(kù)。另外,在瀏覽器中,沒(méi)有可以解決 tree shaking 的打包工具,最終我們會(huì)將整個(gè)庫(kù)代碼發(fā)送給我們的使用者。因此,我們不能像如下代碼引入整個(gè)庫(kù)文件。
<script type="module">
import { Button } from "https://unpkg.com/uilibrary/index.js";
script>
現(xiàn)在,如果我們只是簡(jiǎn)單地將 src 轉(zhuǎn)換為 lib 并將該 lib 托管在 CDN 上,那么我們的使用者實(shí)際上可以得到他們想要的任何東西而沒(méi)有任何開(kāi)銷(xiāo),“代碼更少,加載更快” ?
<script type="module">
import { Button } from "https://unpkg.com/uilibrary/lib/button.js";
script>
Core Packages
Core Packages(核心包)永遠(yuǎn)不會(huì)通過(guò) 標(biāo)記使用,因?yàn)樗鼈儽仨毷侵鲬?yīng)用程序的一部分。因此,我們可以安全地發(fā)布這些軟件包的構(gòu)建版本( UMD,ES),并將構(gòu)建后的系統(tǒng)交給用戶使用。
例如,他們可以使用 UMD 而不使用搖樹(shù)優(yōu)化,或者如果打包器能夠識(shí)別并獲得搖樹(shù)優(yōu)化的好處,則可以使用 ES。
// CJS require
const Button = require("uilibrary/button");
// ES import
import {Button} from "uilibrary";
對(duì)于 UI 庫(kù)
-
當(dāng)我們針對(duì) es 模塊系統(tǒng)構(gòu)建時(shí),需要 Babel 編譯源代碼,并將編譯后的代碼放置在 lib 文件夾中。我們甚至可以將 lib 托管在 CDN 上。 -
當(dāng)我們針對(duì) cjs/umd 模塊系統(tǒng)和 es 模塊系統(tǒng) 等多個(gè)模塊系統(tǒng)構(gòu)建時(shí),需要 rollup ? 打包和壓縮代碼。
下面我們修改 package.json 以指向?qū)?yīng)的模塊系統(tǒng)。
// package.json
{
"name": "js-module-system",
"version": "0.0.1",
// for umd/cjs builds
"main": "dist/index.js",
// for es build
"module": "dist/index.es.js"
}
對(duì)于 core packages,我們不需要 lib 版本。我們只需要針對(duì) cjs/umd 模塊系統(tǒng)和 es 模塊系統(tǒng),使用 rollup 進(jìn)行 ? 打包和壓縮源代碼即可。
提示:對(duì)于愿意通過(guò) 標(biāo)記下載整個(gè)庫(kù)/軟件包的用戶,我們也可以在 CDN 上托管 dist 文件夾。
我們?cè)趺催M(jìn)行打包
我們應(yīng)在在 package.json 中為了不同的目的編寫(xiě)不同的腳本。你能在 GitHub 上面找到 Rollup 的一些配置—— rollup config。
// package.json
{
"scripts": {
"clean": "rimraf dist",
"build": "run-s clean && run-p build:es build:cjs build:lib:es",
"build:es": "NODE_ENV=es rollup -c",
"build:cjs": "NODE_ENV=cjs rollup -c",
"build:lib:es": "BABEL_ENV=es babel src -d lib"
}
}
我們應(yīng)該發(fā)布哪些東西
-
License -
README -
Changelog -
Metadata( "main","module","bin")— package.json -
Control through package.json"files"property
在 package.json 中, "files" 字段是一個(gè)數(shù)組類(lèi)型 ,用來(lái)表示軟件包被當(dāng)做第三方依賴安裝時(shí),都有哪些文件或文件夾需要下載到業(yè)務(wù)項(xiàng)目中。如果你在數(shù)組中加入了一個(gè)文件夾,那么在你 npm install 時(shí),文件夾及下面的文件都會(huì)被下載。
在我的示例項(xiàng)目中,我在 "files" 中加入了 lib 和 dist 文件夾。
// package.json
{
"files": ["dist", "lib"]
}
最后,終于可以準(zhǔn)備發(fā)布了。只需在終端中鍵入 npm run build 命令,你就看到以下輸出。仔細(xì)查看 dist 和 lib 文件夾都有哪些東西。
可以發(fā)布了?
總結(jié)
至此,我們已經(jīng)了解了 JavaScript 模塊系統(tǒng)以及如何創(chuàng)建自己的庫(kù)并發(fā)布它。下面是一些注意事項(xiàng):
1. 是否可以啟用搖樹(shù)優(yōu)化
2. 至少需要構(gòu)建 ES modules and CommonJS 兩種模塊系統(tǒng)
3. 使用 Babel 和 Bundlers 搭建 libraries
4. 使用 Bundlers 搭建 Core packages
5. 在 package.json 中使用 "module" 字段 來(lái)構(gòu)建 es 模塊的版本(PS:這有助于使用 tree shaking)
6. 發(fā)布已編譯的文件夾以及模塊的編譯版
原文鏈接:https://www.freecodecamp.org/news/anatomy-of-js-module-systems-and-building-libraries-fadcd8dbd0e/
作者:Kamlesh Chandnani
譯者:古月
校對(duì)者:水歌

非營(yíng)利組織 freeCodeCamp.org 自 2014 年成立以來(lái),以“幫助人們免費(fèi)學(xué)習(xí)編程”為使命,創(chuàng)建了大量免費(fèi)的編程教程,包括交互式課程、視頻課程、文章等。我們正在幫助全球數(shù)百萬(wàn)人學(xué)習(xí)編程,希望讓世界上每個(gè)人都有機(jī)會(huì)獲得免費(fèi)的優(yōu)質(zhì)的編程教育資源,成為開(kāi)發(fā)者或者運(yùn)用編程去解決問(wèn)題。
你也想成為
freeCodeCamp 社區(qū)的貢獻(xiàn)者嗎
歡迎點(diǎn)擊以下文章了解
