帶你摸清前端依賴管理的歷史程序,深入npm、yarn、pnpm背後的故事

語言: CN / TW / HK

前言

依賴管理作為前端開發不可缺少的部分,在我們的日常工作中非常重要。在快速迭代中,我們幾乎不可能在不依賴開源專案的情況下獨立為專案設計自有的元件庫/請求庫,因為這會大大影響主要功能的開發進度,然而我們的前端專案到底是怎麼做到在不“混亂”的情況下的使用幾百個依賴庫的呢?

Take your JavaScript development up a notch

——www.npmjs.com

🐟 刀耕火種時代

```

```

相信接觸過jquery的同學們一定聽說過bootstrap這個元件庫

在最早的不基於任何前端現代庫(React/Vue等)的原生頁面中,如果我們想快捷的搭建一套b端頁面,bootstrap是一個很好的選擇,你只需要在script標籤中引入對應庫的cdn連結即可,然而由於bootstrap依賴於jquery,我們必須把引入jquery的標籤寫在bootstrap的前面,不然就會導致報錯。

這樣簡單粗暴的依賴管理方法顯然跟不上時代的變化,幾個庫也許可以分清前後依賴關係,那麼如果依賴庫數量增多,這個依賴關係將會變得更加複雜

🐠 npm:白銀時代

亮點

npm是Node.js官方提供的,只要你安裝了node,你就可以使用npm的強大能力,它的出現同時也制定了一些包管理規範:

  • 所有的第三方依賴包都放在node_modules這個檔案目錄下,我們再增加,刪除,升級依賴也只是更新這個檔案下的相關依賴包。
  • package.json檔案中存放本專案及專案的依賴和版本資訊,這樣我們就可以知道本專案用到什麼,都是什麼版本。

至此,我們多一個可以幫我們管理依賴庫的助手,我們再也不用關心我們使用的程式碼庫是否依賴於其他庫,npm替我們託管一切

里程碑

npm 2 遞迴結構

``` ├── node_modules

│ ├── [email protected]

│ │ └── node_modules

│ │ │ └── [email protected]

│ ├── [email protected]

│ │ └── node_modules

│ │ └── [email protected] ```

在安裝依賴包時,採用簡單的遞迴安裝方法。執行 npm install 後,npm 2 依次遞迴安裝 A 和 B 兩個包到 node_modules 中。執行完畢後,我們會看到 ./node_modules 這層目錄只含有這兩個子目錄。

進入更深一層 A 或 B 目錄,將看到這兩個包各自的 node_modules 中,已經由 npm 遞迴地安裝好自身的依賴包。包括 ./node_modules/A/node_modules/D , ./node_modules/B/node_modules/D 等等。而每一個包都有自己的依賴包,每個包自己的依賴都安裝在了自己的 node_modules 中。依賴關係層層遞進,構成了一整個依賴樹,這個依賴樹與檔案系統中的檔案結構樹剛好層層對應

對複雜的工程, node_modules 內目錄結構可能會太深。導致深層的檔案路徑過長而觸發 windows 檔案系統中,檔案路徑不能超過 260 個字元長的錯誤

部分被多個包所依賴的包,很可能在應用 node_modules 目錄中的很多地方被重複安裝。隨著工程規模越來越大,依賴樹越來越複雜,這樣的包情況會越來越多,造成大量的冗餘

如上圖,兩個一級依賴都同時依賴於一個二級依賴,這個庫被下載了兩遍

npm 3 扁平結構

``` ├── node_modules

│ ├── [email protected]

│ ├── [email protected]

│ ├── [email protected] ```

扁平結構相當於把整個node_modules給“拍平”了,npm它會遍歷所有依賴樹節點,逐個將模組放在一級node_modules中,當發現有重複模組時,則將其丟棄

在這種結構中,我們可以省略冗餘的依賴下載,但是遇到同名依賴的不同版本,它似乎變得有些“不懂變通”,下面兩張圖只取決於node_modules 中依賴的書寫順序,case2和case1生成了完全不同的安裝樹(npm後續版本會把最新版本的庫安裝在一級目錄,如果其他庫依賴較低版本才會安裝在自身的node_modules中)

``` case1:

├── node_modules

│ ├── [email protected]

│ ├── [email protected]

│ ├── [email protected]

│ ├── [email protected]

│ │ └── node_modules

│ │ │ └── [email protected]

case2:

├── node_modules

│ ├── [email protected]

│ ├── [email protected]

│ ├── [email protected]

│ │ └── node_modules

│ │ │ └── [email protected]

│ ├── [email protected]

│ │ └── node_modules

│ │ │ └── [email protected] ```

對於 npm 來說同名但不同版本的包是兩個獨立的包,而同層不能有兩個同名子目錄,所以:

  • 在一級node_moudles中已經存在依賴包的情況下,新安裝的依賴包如果存在版本衝突,則仍會安裝到新依賴包的node_modules中。
  • 在一級node_moudles中已經存在依賴包的情況下,新安裝的依賴包如果不存在版本衝突,則會忽略安裝。

npm 5 輔助鎖定

npm 5.x開始,執行npm install時會自動生成一個package-lock.json 檔案。

ps:在2016年yarn推出yarn.lock後npm在2017年才推出了package-lock.json

``` {

"name": "web_offline_widget",

"version": "1.0.0",

"lockfileVersion": 1,

"requires": true,

"dependencies": {

    "accepts": {

        "version": "1.3.7",

        "resolved": "http://bnpm.byted.org/accepts/-/accepts-1.3.7.tgz",

        "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",

        "requires": {

            "mime-types": "~2.1.24",

            "negotiator": "0.6.2"

        }

    },

   //以下省略

```

npm為了讓開發者在安全的前提下使用最新的依賴包,在package.json中通常做了鎖定大版本的操作,這樣在每次npm install的時候都會拉取依賴包大版本下的最新的版本。這種機制最大的一個缺點就是當有依賴包有小版本更新時,可能會出現協同開發者的依賴包不一致的問題

package-lock.json檔案精確描述了node_modules 目錄下所有的包的樹狀依賴結構,每個包的版本號都是完全精確的

因為這個檔案記錄了 node_modules 裡所有包的結構、層級和版本號甚至安裝源,它也就事實上提供了 “儲存” node_modules 狀態的能力。只要有這樣一個 lock 檔案,不管在哪一臺機器上執行 npm install 都會得到完全相同的 node_modules 結果。

Q:為什麼我們的package-lock.json檔案總是在npm install的時候就自己更新了?它到底鎖了什麼?

A:

  • 不一樣版本的 npm 的安裝演算法不一樣。
  • 某些依賴項自上次安裝以來,可能已釋出了新版本,所以將根據 package.json 中的 semver-range version 更新依賴。
  • 某個依賴項的依賴項可能已釋出新版本,即便你使用了固定依賴項說明符(1.2.3 而不是 ^1.2.3),它也會更新。

🐬 yarn:黃金時代

亮點

yarn是Facebook首創,後由獨立組織維護的依賴管理工具

說到yarn就不得不提到它的高速,速度快的理由主要來自以下兩個方面:

  • 並行安裝:無論 npm 還是 Yarn 在執行包的安裝時,都會執行一系列任務。npm 是按照佇列執行每個 package,也就是說必須要等到當前 package 安裝完成之後,才能繼續後面的安裝。而 Yarn 是同步執行所有任務,提高了效能
  • 離線模式:如果之前已經安裝過一個軟體包,用Yarn再次安裝時之間從快取中獲取,就不用像npm那樣再從網路下載了

里程碑

yarn2 無node_modules模式

遷移官方文件: https://yarnpkg.com/getting-started/migration

npm install -g [email protected]

在使用yarn 2.x安裝以後,node_modules不會再出現,代替它的是.yarn目錄,裡面有cache和unplugged兩個目錄,以及外面一個.pnp.js

  • .yarn/cache裡面放所有需要的依賴的壓縮包,zip格式
  • .yarn/unplugged是你需要手動去修改的依賴,使用yarn unplugin lodash可以把lodash解壓到這個目錄下,之後想修改什麼的隨意
  • .pnp.js是PNP功能的核心,所有的依賴定位都需要通過它來

無node_modules模式可以加快專案安裝速度,同時大大縮減刪除一整個專案的速度,node_modules為專案帶來了非常多的節點檔案,僅依賴於React的專案就有將近2w+個節點,刪除操作就變得困難起來

ps.新專案可以嚐鮮去試試yarn2/yarn3,老專案改造異常困難,會報各種各樣的錯誤,改造收益不大,目前中文網際網路有幾乎沒有成熟的相容遷移文件,直接幹掉node_modules對於一個較為複雜的專案來說步子還是邁的太大了

🐳 pnpm:未來時代

亮點

pnpm 相比較於 yarn/npm 這兩個常用的包管理工具在效能上也有了極大的提升

如果我們使用yarn/npm安裝express,我們的node_modules目錄會變成:

而如果使用pnpm,會變成這樣:

node_modules 中只有一個叫 .pnpm 的資料夾以及一個叫做 express 的軟鏈。 不錯,pnpm只安裝了 express,所以它是唯一一個你的應用擁有訪問許可權的包。

express 只是一個軟鏈。 當 Node.js 解析依賴的時候,它使用這些依賴的真實位置,所以它不保留軟鏈。 express 的真實位置在node_modules/.pnpm/[email protected]/node_modules/express裡,我們的.pnpm/ 以真正平鋪的形式儲存著所有的包,所以每個包(包括這個包的所有依賴包)都可以在這種命名模式的資料夾中被找到:.pnpm/<name>@<version>/node_modules/<name>

這個平鋪的結構避免了 npm v2 建立的巢狀 node_modules 引起的長路徑問題,但與 npm v3,4,5,6 或 yarn v1 建立的扁平的 node_modules 不同的是,它保留了包之間的相互隔離,並且真正解決了重複包的問題。

解決Phantom dependencies

Phantom dependencies 被稱之為幽靈依賴,解釋起來很簡單,即某個包沒有被安裝(package.json中並沒有,但是使用者卻能夠引用到這個包)

這個現象的出現原理很好理解,在npm推出v3以後,一個庫只要被其他庫依賴,哪怕沒有顯式宣告在package.json中,也可以會被安裝在node_modules的一級目錄裡,我們可以“自由”的在專案中使用這些幽靈依賴

試想這種case:

``` package.json -> a(b 被 a 依賴)

node_modules

/a

/b ```

那麼這裡這個 b 就成了一個幽靈依賴,如果某天某個版本的 a 依賴不再依賴 b 或者 b 的版本發生了變化,那麼 require b 的模組部分就會拋錯

得益於pnpm的目錄格式,它天生解決了這個幽靈依賴問題,如果不顯式宣告,開發者不可能擁有 b 的使用許可權

提出的規範需要時間踐行

pnpm這麼好,我們可以現在就使用它嗎?答案是 可以 也 不可以

如果是一個從零開始的新專案,那大可放心的用上

但如果是一個有幾年歷史包袱的老專案,如果這個專案的部分依賴庫寫的不夠規範,庫自己就使用了幽靈依賴,那麼將導致在pnpm install的時候報錯

雖然我們實在不行還可以設定shamefully-hoist:ture來豁免幽靈依賴:

但是pnpm的設計初衷還是為了杜絕幽靈依賴的,如果未來工具庫的設計者都遵循標準設計出合理使用依賴的庫,那麼pnpm將會發揮更大的作用。

參考

http://www.javashuo.com/article/p-tdfleods-ks.html

https://juejin.cn/post/6844903601563762702

https://zhuanlan.zhihu.com/p/107343333

https://pnpm.io/zh/blog/2020/05/27/flat-node-modules-is-not-the-only-way