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