了解 JavaScript 模塊系統(tǒng)基礎知識,搭建自己的庫

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

為什么理解 JS 模塊系統(tǒng)很重要
我的日常工作是設計和項目架構,并且我很快意識到跨項目需要許多通用功能。我總是一次又一次地將這些功能復制粘貼到新項目中。
問題是,每當更改一部分代碼時,我都需要在所有項目中手動同步這些更改。為了避免所有這些繁瑣的手動任務,我決定提取通用功能并從中組成一個 NPM 軟件包。這樣,團隊中的其他人將能夠將它們重新用作依賴項,并在每次推出新版本時都可以對其進行更新。
這種方法具有一些優(yōu)點:
-
如果核心庫中有一些更改,則只需在一個地方進行更改,而無需為同一件事重構所有應用程序的代碼。 -
所有應用程序保持同步。無論何時進行更改,所有應用程序僅需要運行 npm update命令。
庫的源碼
因此,下一步是發(fā)布庫。
這是最困難的部分,因為我腦海中突然跳出一堆東西,例如:
-
如何使用搖樹優(yōu)化 -
應該針對哪些 JS 模塊系統(tǒng)(CommonJS、AMD、ES modules) -
需要轉譯源碼嗎 -
需要打包源碼嗎 -
應該發(fā)布哪些文件
在發(fā)布第三方庫(組件庫,工具庫)時,我們每個人的腦海中都應該冒出這些問題。
來, 我們一步步解決以上的問題。
不同類型的 JS 模塊系統(tǒng)
1. CommonJS
-
由 Node.js 實現 -
多用在服務器端安裝模塊時 -
沒有 runtime/async 模塊 -
通過 require導入模塊 -
通過 module.exports導出模塊 -
無法使用搖樹優(yōu)化,因為當你導入時會得到一個模塊時,得到的是一個對象,所以屬性查找在運行時進行,無法靜態(tài)分析 -
會得到一個對象的副本,因此模塊本身不會實時更改 -
循環(huán)依賴的不能優(yōu)雅處理 -
語法簡單
2. AMD 異步模塊定義
-
由 RequireJs 實現 -
當你在客戶端(瀏覽器)環(huán)境中,異步加載模塊時使用 -
通過 require實現導入 -
語法復雜
3. UMD 通用模塊定義
-
CommonJs + AMD 的組合(即 CommonJs 的語法 + AMD 的異步加載) -
可以用于 AMD/CommonJs 環(huán)境。 -
UMD 還支持全局變量定義,因此,UMD 模塊能夠在客戶端和服務器上工作。
4. ES modules
-
用于服務器/客戶端 -
支持模塊的 Runtime/static loading -
當你導入時,獲得是實際對象 -
通過 import導入,通過export導出 -
靜態(tài)分析——你可以決定編譯時的導入和導出(靜態(tài)),你只需要看源碼,不需要執(zhí)行它 -
由于 ES6 支持靜態(tài)分析,因此搖樹優(yōu)化是可行的 -
始終獲取實際值,以便實時更改模塊本身 -
比 CommonJS 有更好的循環(huán)依賴管理
現在,我們了解了不同類型的 JS 模塊系統(tǒng)以及它們如何演變。
盡管所有工具和現代瀏覽器都支持 ES modules,但我們在發(fā)布庫時不知道用戶如何利用我們的庫。因此,我們必須確保我們的庫在所有環(huán)境中都能正常工作。
讓我們深入研究并設計一個示例庫,更好地回答與發(fā)布庫有關的所有問題。
我已經建立了一個小型的 UI 庫(你可以在 GitHub 上找到源代碼),并且我將分享我在編譯,打包和發(fā)布中的所有經驗和探索。
目錄結構
在這里,我們有一個小的 UI 庫,其中包含 3 個組件:Button,Card 和 NavBar。讓我們一步步進行編譯并發(fā)布。
發(fā)布前的最佳實踐
1. 搖樹優(yōu)化(Tree Shaking)
webpack 官方文檔有說明
-
搖樹優(yōu)化是一個術語,通常用于描述移除 JavaScript 上下文中的未引用代碼(dead-code)。它依賴于 ES2015 模塊系統(tǒng)中的靜態(tài)結構特性,例如 [import][4]和[export][5]。這個術語和概念實際上是興起于 ES2015 模塊打包工具 rollup。新的 webpack 4 正式版本,擴展了這個檢測能力,通過package.json的"sideEffects"屬性作為標記,向 compiler 提供提示,表明項目中的哪些文件是純的 ES2015 模塊,由此可以安全地刪除文件中未使用的部分。 -
webpack 和 Rollup 都支持搖樹優(yōu)化,這意味著我們需要牢記某些事情,以便我們的代碼可被 Tree Shaking。
2. 發(fā)布所有模塊形態(tài)
-
我們應該發(fā)布所有模塊形態(tài),例如 UMD 和 ES Module,因為我們永遠不知道用戶在哪個版本的瀏覽器或 webpack 中使用此庫/包。 -
即使所有打包程序(如 webpack 和 Rollup)都能解析 ES Module ,但如果我們的使用者使用的是 webpack 1.x,則它無法解析 ES 模塊。
// package.json
{
"name": "js-module-system",
"version": "0.0.1",
-
package.json 文件的 main 字段通常用于指向 UMD 版本的庫/包。 -
package.jso 文件的 module 字段用于指向 ES 版本的庫/包。
鮮為人知的事實:webpack 使用 resolve.mainfields 確定檢查
package.json中的哪些字段。
性能提示:由于所有現代瀏覽器現在都支持 ES 模塊,因此也請務必發(fā)布 ES 版本的庫/包。這樣一來,可以減少編譯次數,最終可以減少向用戶交付的代碼。這將提高應用程序的性能。
那么,下一步是什么?編譯還是打包?我們應該使用什么工具?啊,這是最棘手的部分!讓我們深入研究研究。
webpack vs Rollup vs Babel
這些我們在日常工作中使用的工具,用于承載我們的應用程序/庫/軟件包。沒有它們,我無法想象現代的 Web 開發(fā)有多么糟糕。因此,我們無法將它們進行比較 ?
每種工具都有其自身的優(yōu)勢,并根據使用者的需求達到不同的目的。
現在讓我們看一下這些工具:
webpack
webpack 是一個很棒的模塊打包工具, 它被廣泛接受并且主要用于構建 SPA。它提供了開箱即用的所有功能,例如代碼拆分、按需加載、搖樹優(yōu)化等,并且它本身使用的是 CommonJS 模塊系統(tǒng)。
RollupJS
RollupJS 還是類似于 webpack 的模塊打包器。但是,RollupJS 的主要優(yōu)點是它遵循 ES6 修訂版中包含的代碼模塊的新標準化格式,因此你可以使用它來打包 ES module variant 的 library/package,但它不支持按需加載。
Babel
Babel 是 JavaScript 的編譯器,以將 ES6 代碼轉換為可在你的瀏覽器(或服務器)中運行的代碼而聞名。請記住,它只是編譯而不會打包你的代碼。
我的建議:對庫使用 Rollup.js,對應用程序使用 webpack。
編譯(Babel-ify)源代碼還是直接打包源代碼
在構建我的 NPM 庫時,我花費了大量時間來試圖找出該問題(如何編譯、如何打包)的答案。我開始挖掘自己的 node_modules,查找所有優(yōu)秀的庫并檢查它們的構建系統(tǒng)。

對比 libraries/packages 構建的輸出
在查看了不同 libraries/packages 的構建輸出之后,我清楚地了解了這些庫的作者在發(fā)布之前可能會想到的不同策略。以下是我的觀察。
如你在上圖中所看到的,我已根據它們的特性將這些庫/軟件包分為兩組:
-
UI Libraries-UI 庫( styled-components,material-ui) -
Core Packages-核心包( react,react-dom)
你可能已經弄清楚了這兩組之間的區(qū)別。
UI Libraries
-
有一個 dist 文件夾,該文件夾是針對 ES 和 UMD/CJS 模塊系統(tǒng) 的打包和壓縮版本。 -
有一個 lib 文件夾,用來存放被編譯后的代碼。
Core Packages
-
只有一個文件夾,其中包含針對 CJS 或 UMD 模塊系統(tǒng)的打包和壓縮版本。
但是,為什么 UI Libraries 和 Core Packages 的構建輸出有所不同?
UI Libraries
想象一下,如果我們只是發(fā)布庫的 bundled version 將其托管在 CDN 上,我們的用戶將直接在 標記中使用它?,F在,如果使用者只想使用 組件,則他們必須加載整個庫。另外,在瀏覽器中,沒有可以解決 tree shaking 的打包工具,最終我們會將整個庫代碼發(fā)送給我們的使用者。因此,我們不能像如下代碼引入整個庫文件。
<script type="module">
import { Button } from "https://unpkg.com/uilibrary/index.js";
script>
現在,如果我們只是簡單地將 src 轉換為 lib 并將該 lib 托管在 CDN 上,那么我們的使用者實際上可以得到他們想要的任何東西而沒有任何開銷,“代碼更少,加載更快” ?
<script type="module">
import { Button } from "https://unpkg.com/uilibrary/lib/button.js";
script>
Core Packages
Core Packages(核心包)永遠不會通過 標記使用,因為它們必須是主應用程序的一部分。因此,我們可以安全地發(fā)布這些軟件包的構建版本( UMD,ES),并將構建后的系統(tǒng)交給用戶使用。
例如,他們可以使用 UMD 而不使用搖樹優(yōu)化,或者如果打包器能夠識別并獲得搖樹優(yōu)化的好處,則可以使用 ES。
// CJS require
const Button = require("uilibrary/button");
// ES import
import {Button} from "uilibrary";
對于 UI 庫
-
當我們針對 es 模塊系統(tǒng)構建時,需要 Babel 編譯源代碼,并將編譯后的代碼放置在 lib 文件夾中。我們甚至可以將 lib 托管在 CDN 上。 -
當我們針對 cjs/umd 模塊系統(tǒng)和 es 模塊系統(tǒng) 等多個模塊系統(tǒng)構建時,需要 rollup ? 打包和壓縮代碼。
下面我們修改 package.json 以指向對應的模塊系統(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"
}
對于 core packages,我們不需要 lib 版本。我們只需要針對 cjs/umd 模塊系統(tǒng)和 es 模塊系統(tǒng),使用 rollup 進行 ? 打包和壓縮源代碼即可。
提示:對于愿意通過 標記下載整個庫/軟件包的用戶,我們也可以在 CDN 上托管 dist 文件夾。
我們怎么進行打包
我們應在在 package.json 中為了不同的目的編寫不同的腳本。你能在 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"
}
}
我們應該發(fā)布哪些東西
-
License -
README -
Changelog -
Metadata( "main","module","bin")— package.json -
Control through package.json"files"property
在 package.json 中, "files" 字段是一個數組類型 ,用來表示軟件包被當做第三方依賴安裝時,都有哪些文件或文件夾需要下載到業(yè)務項目中。如果你在數組中加入了一個文件夾,那么在你 npm install 時,文件夾及下面的文件都會被下載。
在我的示例項目中,我在 "files" 中加入了 lib 和 dist 文件夾。
// package.json
{
"files": ["dist", "lib"]
}
最后,終于可以準備發(fā)布了。只需在終端中鍵入 npm run build 命令,你就看到以下輸出。仔細查看 dist 和 lib 文件夾都有哪些東西。
可以發(fā)布了?
總結
至此,我們已經了解了 JavaScript 模塊系統(tǒng)以及如何創(chuàng)建自己的庫并發(fā)布它。下面是一些注意事項:
1. 是否可以啟用搖樹優(yōu)化
2. 至少需要構建 ES modules and CommonJS 兩種模塊系統(tǒng)
3. 使用 Babel 和 Bundlers 搭建 libraries
4. 使用 Bundlers 搭建 Core packages
5. 在 package.json 中使用 "module" 字段 來構建 es 模塊的版本(PS:這有助于使用 tree shaking)
6. 發(fā)布已編譯的文件夾以及模塊的編譯版
原文鏈接:https://www.freecodecamp.org/news/anatomy-of-js-module-systems-and-building-libraries-fadcd8dbd0e/
作者:Kamlesh Chandnani
譯者:古月
校對者:水歌

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