本文比較了NPM、Yarn 和 pnpm 三種包管理工具的特點,重點分析了它們在安裝速度、依賴管理、磁碟空間使用、依賴衝突處理等方面的差異,重點介紹了pnpm的依賴組織結構。 ...
JavaScript 應用程式通常依賴於許多外部庫,這些依賴項通常通過包管理器來管理。預設情況下,Node.js 使用 NPM 作為包管理器。
由於早期的 NPM 存在各種不足,社區後來開發了 Yarn 和 pnpm 作為替代品。
如果要使用 Yarn 和 pnpm,則需要先通過 NPM 進行安裝。
早期 NPM 的不足
-
依賴樹過深
在 NPM 3.0 之前,NPM 使用了嵌套依賴樹的結構。這意味著如果一個項目的多個依賴項需要同一個包的不同版本,NPM 會在每個依賴項的目錄中重覆安裝該包。這種結構會導致
node_modules
目錄非常深,特別是在 Windows 系統中,這可能導致路徑長度限制的問題。 -
重覆安裝和磁碟空間浪費
每次安裝包時都會重新從頭開始解決依賴關係,並逐個下載和安裝包。即使是已經安裝過的包,也可能會再次下載,而沒有利用緩存機制。這種重覆安裝的策略會導致安裝十分緩慢。
-
依賴版本不確定
在早期版本的 NPM 中,沒有類似
yarn.lock
或package-lock.json
這樣的鎖文件。這意味著即使package.json
中指定了版本範圍(例如^1.0.0
這種表示可以接受一個範圍的版本),依賴關係的解析和安裝仍然是動態的,可能會因為時間或網路狀態的不同而導致不同的版本被安裝。 -
扁平化依賴樹
NPM 在 3.0 版本引入了扁平化依賴樹,以解決早期版本中嵌套依賴樹帶來的問題,但是扁平化依賴帶來了新的問題。
- 依賴衝突:扁平化依賴樹的設計將所有依賴項都安裝在項目的根
node_modules
目錄中,這意味著多個包可能會共用同一個依賴項的版本。如果不同的包需要不同版本的相同依賴項,就可能會發生衝突。 - 幽靈依賴:依賴的依賴被平鋪在根
node_modules
目錄中,這意味著即使應用的package.json
中沒有聲明的依賴,也可以被引入並使用。這種現象會導致依賴關係和依賴版本的不明確。
圖中的虛線就代表幽靈依賴,也叫隱式依賴。
依賴 E 原本是 B 的依賴,但是被扁平化後提升到 node_modules 頂層。
這個 E 沒有被顯式地在 package.json 中聲明,但是結合
node.js
的模塊解析機制可知這個依賴是可以被 Project 引入的。這種 意料之外 的依賴關係會使得項目難以維護。
- 依賴衝突:扁平化依賴樹的設計將所有依賴項都安裝在項目的根
Yarn
Yarn的提出是為瞭解決 NPM 的不足,它具有以下特點:
-
確定性安裝
Yarn 引入了
yarn.lock
鎖文件,明確了依賴的版本。 -
更快更小
Yarn 通過並行下載以及引入緩存機制來加快安裝速度,並且由於緩存的存在,在離線狀態下也可以安裝已緩存過的依賴。
-
扁平化依賴結構
減少了路徑深度,提高了依賴解析的速度。解決了依賴衝突問題:Yarn 會通過將不同版本的依賴項放在各自子目錄的
node_modules
中來解決衝突,而不是強制將所有依賴都安裝在頂層。 -
可以通過配置 workspaces 支持 monorepo。
不足:
- 沒有解決幽靈依賴的問題;
- workspaces 配置較繁瑣。
pnpm
pnpm的特點:
-
節省磁碟空間
npm 和 Yarn 會在每個項目的
node_modules
目錄中為所有依賴項存儲完整的文件副本。如果有多個項目依賴相同的包,那麼這些包會被重覆存儲。pnpm 使用中心化的 store 統一存儲安裝的包,項目內的依賴通過鏈接指向 store 中的依賴。如果有多個項目依賴相同的包,都指向 store 中單一的包。
-
安裝速度更快
pnpm npm/yarn/... 1. 依賴解析。 倉庫中沒有的依賴都被識別並獲取到倉庫。
2. 目錄結構計算。node_modules
目錄結構是根據依賴計算出來的。
3. 鏈接依賴項。 所有以前安裝過的依賴項都會直接從倉庫中獲取並鏈接到node_modules
。1. 解析所有依賴。
2. 獲取所有依賴。
3. 將所有依賴寫入node_modules
。pnpm 的中心化store可以更大程度地復用依賴包,使得安裝依賴這一步驟更快完成。
-
支持 monorepo,配置比起 yarn 來說相對簡單,並且得益於 pnpm 的特性,安裝依賴很快。
-
非扁平化的 node_modules
上文說到 yarn 和 npm 為瞭解決路徑過長、依賴管理複雜等問題,將依賴進行扁平化管理。但是也帶來了幽靈依賴等新問題。
pnpm 的創新點在於提出了 基於符號鏈接的非扁平化 node_modules 結構,解決了幽靈依賴問題。
硬鏈接和軟鏈接
在 Linux 操作系統中,每一個文件對應一個 inode(索引節點)。鏈接是一種在共用文件和訪問它的用戶的若幹目錄項之間建立聯繫的一種方法。
- 硬鏈接是文件的別名,和源文件指向同一個 inode。即硬鏈接和源文件是同一個文件。
- 軟連接也叫符號鏈接,是一種特殊的文件類型,其中包含對另一個文件的引用。軟鏈接可以看作是對一個文件的間接指針,類似於 Windows 操作系統下的 快捷方式 。即軟鏈接和源文件是不同文件。
在 Windows 中也有軟硬鏈接的概念,在 cmd 中通過
mklink
指令創建鏈接:-
硬鏈接:
mklink /H link_name target_file
-
軟鏈接
mklink link_name target_file
pnpm的node_modules結構
文件結構示例:comparing-node-modules/pnpm5-example at master · zkochan/comparing-node-modules (github.com)
pnpm將實際的依賴文件都安裝到全局store中,在項目中的
node_modules
文件夾內通過創建鏈接來使用store中的依賴。與 yarn 和 npm 直接將所有依賴平鋪在 node_modules 中的做法不同,pnpm 在 node_modules 中創建了一個
.pnpm
文件夾,再將所有依賴都平鋪在這個文件夾中。這樣 node.js 的模塊解析演算法就無法引入非頂層依賴了,故解決了幽靈依賴問題。.pnpm
中的依賴通過軟鏈接建立依賴之間的父子關係,並通過硬鏈接指向實際存在於全局store中的依賴包。在 package.json 中顯式聲明的依賴會通過軟鏈接提升到 node_modules 文件夾下,因此 node.js 可以正常解析 package.json 中聲明的依賴。
在
.pnpm
中,依賴通過.pnpm/<name>@<version>/node_modules/<name>
的形式進行記錄,可以看到同一個包的不同版本會被分開記錄。如上圖,項目中只有 express 這一個依賴,而 express 有許多子依賴,這裡只列舉了 qs 這一個依賴。
可以觀察到,這種基於鏈接的 node_modules 結構實現了:
- 項目的 node_modules 只能解析到 package.json 中顯式聲明的依賴,解決了幽靈依賴問題;
- 所有依賴都被平鋪在
.pnpm
文件夾中,不會導致過長的文件路徑; - 實際的依賴被安裝在全局的store中,項目中僅通過硬鏈接進行關聯,節省了磁碟空間;
- 觀察到
express
和它的依賴同屬於一個文件夾層級(圖中藍色區域),express
所有的依賴都軟鏈至了node_modules/.pnpm/
中的對應目錄。 把express
的依賴放置在同一級別避免了迴圈的軟鏈。
現在的 NPM
yarn 和 pnpm 屬於社區產物,NPM 作為官方的包管理器,一直在吸收社區好物的優點。
現在的 NPM 也有了鎖文件來明確依賴的版本,並且也通過使用緩存、改進依賴解析演算法等手段加速了安裝。
NPM 在 7.0 版本之後也支持配置 monorepo 了,可以在 package.json 中直接配置,但是只支持一些簡單的功能。yarn 則提供了插件系統。
總結
特點 | NPM | Yarn | pnpm |
---|---|---|---|
安裝速度 | 較慢 | 較快 | 大部分情況下比 Yarn 塊 |
依賴管理 | 直接安裝到 node_modules |
通過緩存加速安裝 | 中心化 store,依賴通過符號鏈接安裝 |
磁碟空間使用 | 高 | 中等 | 最低,通過去重和鏈接機制 |
依賴衝突處理 | 容易出現衝突 | 通過鎖文件和解析依賴減少衝突 | 嚴格隔離各依賴版本,減少衝突 |
鎖文件 | package-lock.json |
yarn.lock |
pnpm-lock.yaml |
幽靈依賴問題 | 可能發生 | 可能發生 | 嚴格依賴樹,避免幽靈依賴 |
monorepo支持 | 基礎支持 | 功能豐富,包含插件系統 | 高效的工作空間管理,模塊共用更優化 |
安裝一致性 | 可能由於緩存和平臺差異而不一致 | 高,一致性較好 | 更高,通過全局硬鏈接機制確保一致性 |
性能對比圖像來自 pnpm 官方文檔:Benchmarks of JavaScript Package Managers | pnpm中文文檔 | pnpm中文網