iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 24
1

package

在現代的網站中,使用他人開放原始碼的套件輔助開發已經是稀鬆平常的事情,無論是透過套件加速堆砌產品,或是在開發環境中加上協助工程師的各式工具,只需要稍加設定,一個專案便能輕易加載了成千上萬的外部程式;但如此方便的機制,究竟是怎麼實現的呢?

套件管理工具

對前端開發者來說,最熟悉的應該是安裝 Node.js 時自動附帶的 npm 吧?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 中設定專案欲使用的套件版本,例如下列的例子:

"dendencies": {
    "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

除了預設的 npm,另一個廣為人知的套件管理工具是由 Facebook 開源出來的 Yarn,會聲名大噪的原因,一方面是 Facebook 及眾多開源大神共同開發的品牌效應,另外也因為早期的 npm (< npm @5)在套件版本控制上,時常沒有正確的效果,而當時的 Yarn 就透過一個 yarn.lock 檔,鎖定所有依賴套件的版本,完美解決了這個問題;但現在版本的 npm 也效仿 Yarn 的作法,預設會產出 package-lock.json 來固定套件版本,當時的嚴重缺陷也早已被解決,整體來說,使用 Yarn 的帶來的便利性也已經沒有那麼大,就筆者目前的感受,是沒有太大明顯差異的。

套件管理在成熟專案的重要性早已不言而喻,除了前述的 npm 及 Yarn,一直以來也有許多開發者投身其中,鑽研出其他的套件管理工具;例如 Bowervolopnpm 等等,有興趣的讀者也可以個別深入研究,這邊就不詳述了。

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 非常容易莫名的塞好塞滿,長成誇張的容量黑洞。

https://ithelp.ithome.com.tw/upload/images/20191010/20111380t9EkQTYpmS.jpg

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

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

套件管理的未來

npm 很方便,但也有著不少問題;npm 的前核心開發者 Kat MarchánJSConf 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 安裝套件,並經過 打包工具 的處理,在 前端三大框架 的普及之下,都已經是基本功了;但如同本系列文一直以來所強調的,使用工具的同時,還是要理解為何而用,以及其背後的機制,才能夠真正的控制並活用工具。

那麼以上就是今天的內容啦,明天就繼續往前端必須知道的後端知識前進吧!

參考資料

筆者

Gary

半路出家網站工程師;半生熟的前端加上一點點的後端。
喜歡音樂,喜歡學習、分享,也喜歡當個遊戲宅。

相信一切安排都是最好的路。


上一篇
23. [FE] 網頁的快取機制是怎麼運作的?
下一篇
25. [BE] Node.js 與 JavaScript 的關係是什麼?
系列文
前端三十 - 成為更好的前端工程師31

尚未有邦友留言

立即登入留言