高性能的包管理器Pnpm,你學會了嗎?
概念
performant npm。高性能的 npm。它的 slogan 是:
Fast, disk space efficient package manager。
快速的,節省磁盤空間的包管理工具。
特點
快速。pnpm 比替代方案快 2 倍數據來源[1]
- 高效。Node_modules 中的文件是從一個單一的可內容尋址的存儲中鏈接過來的??梢岳斫獬梢粋€全局的 store 中獲取,后面會詳細提到。
- 支持 monorepos。pnpm 內置支持了單倉多包。類似 --filter 后面接子 package 的 name 表示只把安裝的新包裝入這個 package 中等。簡單實踐參考[2]。
- 嚴格。pnpm 默認創建了一個非平鋪的 node_modules,因此代碼無法訪問任意包。
npm 和 yarn 包管理機制
npm@3 之前
采用的是一種嵌套安裝的方式。如下圖所示:
node_modules
└─ foo
├─ index.js
├─ package.json
└─ node_modules
└─ bar
├─ index.js
└─ package.json
缺點:
- package 中經常創建太深的依賴樹,這會導致 Windows 上的目錄路徑過長問題。
- 當一個 package 在不同的依賴項中需要時,它會被多次復制粘貼并生成多份文件。
npm@3+ 以及 Yarn
將依賴偏平化:
node_modules
├─ foo
| ├─ index.js
| └─ package.json
└─ bar
├─ index.js
└─ package.json
缺點:
- 幻影依賴(Phantom dependencies)。幻影依賴指的是 node_modules 中的依賴包在沒有 package.json 。 中聲明的情況下使用了其他包的依賴
- 依賴結構的不確定性。這里為什么是 D@2.0.0 提升,而不是 D@10.0?都有可能,跟安裝的順序有關。詳情可參考[3]。避免這個問題的解決方案:lock 文件。
- npm 包分身。同樣的也因為打平了 node_modules 中的依賴,就會造成了相同版本的子依賴包在被不同的項目依賴所依賴時會安裝兩次(即上面的圖,B/C 兩個包都依賴了 D@2.0.0)。
安裝很慢。相同的包安裝了兩次,占用磁盤空間,相對的安裝的速度也會變慢。
非單例。當兩個不同的組件調用 require("library-f") 時,它們可能會得到兩個不同的庫實例,這意味著可能會突然出現兩個單例的實例(換言之,底層的 “global” 變量被分配到兩個不同的閉包中)。會使我們的調試變得非常困難。
pnpm 的解決方案
前置知識
inode
每一個文件都有一個唯一的 inode,它包含文件的元信息,在訪問文件時,對應的元信息會被 copy 到內存去實現文件的訪問。
可以通過 stat 命令去查看某個文件的元信息。
stat README.md
hard link
硬鏈接可以理解為是一個相互的指針,創建的 hardlink 指向源文件的 inode,系統并不為它重新分配 inode。硬鏈接不管有多少個,都指向的是同一個 inode 節點,這意味著當你修改源文件或者鏈接文件的時候,都會做同步的修改。每新建一個 hardlink 會把節點連接數增加,只要節點的鏈接數非零,文件就一直存在,不管你刪除的是源文件還是 hradlink。只要有一個存在,文件就存在。
.pnpm 中的每個文件都是來自內容可尋址存儲的硬鏈接。
soft link
軟鏈接可以理解為是一個單向指針,是一個獨立的文件且擁有獨立的 inode,永遠指向源文件,這就類比于 Windows 系統的快捷方式。刪除源文件,軟鏈接就會失效。
修改了軟鏈接或硬鏈接的文件,另外的硬鏈接或軟鏈接以及源文件都會發生變化,這里感覺是需要小心的,特別是修改文件以調試的時候,記得還原回去,否則另外一個項目用到的時候,可能會出問題。
幾個重點結果表現
項目根目錄下的 node_modules 中
node_modules 中只有直接依賴的包,而沒有間接依賴的包。通過軟鏈接到.pnpm 目錄中。
.pnpm
虛擬存儲目錄——.pnpm,所有直接和間接依賴項都鏈接到此目錄中。該目錄通過 @ 來實現相同模塊不同版本之間隔離和復用。
Store
pnpm在全局通過Store來存儲所有的 node_modules 依賴,并且在 .pnpm 中存儲項目的hard links。
在使用 pnpm 對項目安裝依賴的時候,如果某個依賴在 sotre 目錄中存在了話,那么就會直接從 store 目錄里面去 hard-link,避免了二次安裝帶來的時間消耗,如果依賴在 store 目錄里面不存在的話,就會去下載一次。
假如全局的包變得非常大怎么辦?使用方法為 pnpm store prune ,它提供了一種用于刪除一些不被全局項目所引用到的 packages 的功能,例如有個包 axios@1.0.0 被一個項目所引用了,但是某次修改使得項目里這個包被更新到了 1.0.1 ,那么 store 里面的 1.0.0 的 axios 就就成了個不被引用的包,執行 pnpm store prune 就可以在 store 里面刪掉它了。
原理分析
我們來看一張原理圖:
我們項目中有一個依賴 bar@1.0.0。bar@1.0.0也有一個依賴 foo@1.0.0。
- node_modules 下面有 bar@1.0.0 和 .pnpm 目錄,沒有 foo@1.0.0。
- bar@1.0.0 通過軟鏈接指向 .pnpm/bar@1.0.0/node_modules/bar@1.0.0。.pnpm/bar@1.0.0/node_modules/bar@1.0.0 又通過硬鏈接指向 Store。
- bar@1.0.0 依賴的foo@1.0.0 會安裝在跟自己的同一級,這里的設計,我理解是根據 node 的 require 機制,bar 中 require('foo') 的時候,就會先找到 foo@1.0.0,而不會往上尋找,這樣就避免依賴包版本不一致的問題。.pnpm/bar@1.0.0/node_modules/foo@1.0.0。并通過軟鏈接指向 。
- pnpm 下一級的 foo@1.0.0。
.pnpm/foo@1.0.0 一樣通過硬鏈接指向 Store。
遷移和問題
我們現在可能用的是 npm 或者 yarn,那我們如何更好的過渡到 pnpm?或者會不會有什么問題?
- 遷移:
- 遷移 lock 文件??梢酝ㄟ^ pnpm import 的方式。參考[4]。
- 只允許使用 pnpm。參考[5]。
- 解決沖突。跟 npm 和 yarn 一樣。只需要解決完 package.json 的沖突,然后重新 install 即可。
- more...。
問題:
- CI/CD 中全局存儲的問題。可能會命中不同的機器,也有可能存在權限的問題。
- 相比 npm、yarn。社區還沒那么活躍。
- 硬鏈接在 window 系統有兼容性的問題。
- more…。
總結
pnpm 通過巧妙硬鏈接 + 軟鏈接結合的方式完全實現了依賴樹結構的 node_modules,并且嚴格遵循了 Node.js 的模塊解析標準,解決了幻影依賴和 npm 分身的問題。并且通過全局只保存一份在 ~/.pnpm-store 的方式,在不同的項目中進行 install 的速度也會變得更快,也解決了磁盤空間占用的問題。
參考資料
pnpm: 最先進的包管理工具[6]
中文官網[7]
npm 存在的問題以及 pnpm 是怎么處理的[8]
[1]數據來源: https://github.com/pnpm/benchmarks-of-javascript-package-managers
[2]簡單實踐參考: https://zhuanlan.zhihu.com/p/373935751
[3]參考: http://npm.github.io/how-npm-works-docs/npm3/non-determinism.htm
l[4]參考: https://pnpm.io/zh/cli/import
[5]參考: https://pnpm.io/zh/only-allow-pnpm[6]pnpm: 最先進的包管理工具: https://www.aisoutu.com/a/1218460
[7]中文官網: https://www.pnpm.cn/
[8]npm 存在的問題以及 pnpm 是怎么處理的: https://www.yuexunjiang.me/blog/problems-with-npm-and-how-pnpm-handles-them/