現代網站中,使用他人開源套件輔助開發已是常態。無論是用套件加速堆砌產品,或加上協助工程師的開發工具,只需稍加設定,專案便能輕易載入成千上萬的外部程式。這套機制是如何實現的?

套件管理工具#

對前端開發者來說,最熟悉的應該是安裝 Node.js 時自動附帶的 npm(Node Package Manager)吧?npm 即為 Node Package Manager 的縮寫,開發者可以透過 Node 隨附的 npm cli,進行套件的安裝及管理。

例如在專案資料夾的終端機中輸入 npm install express,npm 便會自動從 Registry 中尋找 express 這個非常熱門的 Node.js Web Server 框架,取得最新版本,下載到專案中的 node_modules 資料夾中。

然而在專案中,不可能每次都透過開發者自行指定套件安裝,不但無法管理,也很沒有效率;開發者可以透過專案中的 package.json,羅列出專案需要哪些套件,之後安裝時只需要 npm install,npm 便會自動依照 package.json 的內容下載套件。

版本控制#

由於套件本身也是由開發者撰寫,也需要持續的更新迭代,公開出來的套件往往不會只有一個版本,套件管理工具自然也要能紀錄並提供各版本的套件供開發者使用;在 npm 的設計中,要求套件開發者透過 Semantic Versioning 的規範來定義套件版本:

  • 主版號:當你做了 不相容 的 API 修改
  • 次版號:當你做了向下相容的 功能性新增
  • 修訂號:當你做了向下相容的 問題修正

而使用套件的開發者,則需透過指定的字元,在 package.json 中設定專案欲使用的套件版本,例如下列的例子:

"dependencies": {
    "accepts": "~1.3.7",
    "array-flatten": "1.1.1",
    "body-parser": "^1.19.0"
}
  • 1.1.1:指定為 1.1.1 的版本
  • ~1.3.7:指定為 >= 1.3.7 且 < 1.4.0 的版本
  • ^1.19.0:指定為 >= 1.19.0 且 < 2.0.0 的版本

Yarn#

另一個廣為人知的套件管理工具是 Facebook 開源的 Yarn。聲名大噪的原因有二:

  • Facebook 與眾多開源大神共同開發的品牌效應
  • 早期 npm(< npm @5)在套件版本控制上常出問題,Yarn 透過 yarn.lock 完美解決

現在 npm 也效仿 Yarn,預設產出 package-lock.json 來固定套件版本,當年的嚴重缺陷已被解決。整體來看,Yarn 帶來的便利性已不再明顯。

套件管理在成熟專案的重要性早已不言而喻,除 npm 與 Yarn 外,還有 Bower、volo、pnpm 等其他工具,這裡不一一詳述。

Install 的執行過程#

那麼來進入正題吧,在透過這些套件管理工具進行安裝時,背後的機制究竟是什麼呢?下面將執行 npm install 的過程拆成五個步驟,也許各家套件管理工具實作會略有不同,但不外乎都會經過這些階段:

1. 計算缺少的套件#

npm 會從專案中的三隻檔案,計算出本次 npm install 需要重新下載安裝的內容:

  • 專案內的 node_modules 結構
  • 開發者設定的 package.json
  • npm install 後自動生成的 package-lock.json

由於 package.json 中的套件版本可能會是使用 Semantic Versioning 描述的,npm 需要以 package.json 描述的版本為基礎,與 node_modules 及 package-lock.json 相互比對後,才能計算出需要更新的套件。

2. 從 Registry 取得套件資訊#

計算出來缺少套件清單後,npm 向指定的 Registry 取得各目標套件的 package.json、查詢可用版本,並解析出下載 URL。

3. 計算差異#

由於套件本身也是專案,也可能引用其他套件,不同的套件引用到相同的套件這種事,自然也是稀鬆平常;npm 在這步驟會去計算各套件的 package.json,整理各套件個別需要下載的版本(有可能同套件需要多版本),最後產出整個專案所使用的套件結構樹(package-lock.json)。

4. 下載、提取真正需要的套件#

有了前面這麼多步驟的整理,接著就開始依序下載套件,並將下載的內容解壓縮,提取到 node_modules 資料夾中;這個步驟是 npm install 需時最長、最耗效能的步驟,主要是因為 下載、解壓縮、寫入硬碟分別需要網路、CPU 及硬碟 IO 的支撐,只要硬體裝置的其中一環資源較缺乏,開發者馬上就會有感,速度自然也就快不起來。

為了解決這個問題,如同網頁快取的概念,npm 本身也擁有本地快取機制,在寫入到 node_modules 時,同時會寫入一份到電腦的本地快取中,未來如果有其他專案需要用到同一個版本的套件,npm 會在向 Registry 確認版本未更新(ETag 相符,回傳 304)後,直接複製快取的套件到 node_modules。

5. 執行每個套件的 install#

檔案全部都寫入到 node_modules 後,npm 會執行所有套件的 npm install,讓套件本身的依賴被正確的連結到下載的套件上,並觸發各套件 install、postinstall 的 npm-scripts hook,最後完成 npm install 指令。

安全性問題#

注意到了嗎?前述執行的過程中,即使在 npm 有快取的情況下,仍然不會使用同一份檔案,而是複製一份套件到 node_modules 中;也因為這樣的特性,加上套件依賴套件再依賴套件的層層相依,node_modules 非常容易莫名的塞好塞滿,長成誇張的容量黑洞。

除此之外,層層相依同時也帶來了風險,開發者很容易在不知情中安裝了有風險,甚至含有惡意程式的套件,從 getcookies、Event-Stream,到 bb-builder,甚至是熱門的工具庫套件 lodash 先前都有資安風險,例子不勝枚舉。

開發者在使用套件時,還是要盡可能掌握清楚專案中所使用的套件,並透過 Semantic Versioning 指定套件版本,以及將 package-lock.json 放進版控,徹底將套件版本鎖住,以避免重要的專案莫名遭到池魚之殃。

套件管理的未來#

npm 很方便,但也有著不少問題;npm 的前核心開發者 Kat Marchán 在 JSConf EU 2019 正式公開了 Tink,號稱是下一代的套件管理工具,它的核心概念是利用 JavaScript 直譯的特性,將套件管理的機制從編譯階段移到執行階段,也如同其他程式語言的套件管理方式,將全部專案的套件統一管理,讓專案中幾乎不再需要獨立的 node_modules 資料夾。

“/node_modules” is massive. And is also where dreams go to die. – Kat Marchán, 2019.

看起來一切美好,加上 npm 也將 Tink 排進了未來的 Roadmap 上;雖然目前還只在相對早期的開發狀態,但仍然非常值得開發者持續關注後續發展!

結語#

現代前端透過 npm 安裝套件、經過打包工具處理,在三大框架普及下已是基本功。但如本系列一貫強調的:使用工具時也要理解為何而用,以及背後的機制,才能真正控制並活用工具。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10226839