眾生皆苦,我選pnpm

概述

pnpm - 速度快、節(jié)省磁盤空間的軟件包管理器

perfomance npm ,即 pnpm (高性能npm)

優(yōu)勢(shì)

  • 快速
    • pnpm 是同類工具速度的將近 2 倍
  • 高效
    • node_modules 中的所有文件均鏈接自單一存儲(chǔ)位置
  • 支持monorepos
    • pnpm 內(nèi)置了對(duì)單個(gè)源碼倉庫中包含多個(gè)軟件包的支持

注:這個(gè)東西這么讀monorepos = Monolithic repository /?m?n??liTHik/ /r??p?z??t?rē/

  • 嚴(yán)格
    • pnpm 創(chuàng)建的 node_modules 默認(rèn)并非扁平結(jié)構(gòu),因此代碼無法對(duì)任意軟件包進(jìn)行訪問

以上是4條優(yōu)勢(shì)是官網(wǎng)的說明和宣傳,后面我們會(huì)針對(duì)npm的發(fā)展歷史中存在的問題

來對(duì)比說明,pnpm的提出動(dòng)機(jī),pnpm 的優(yōu)勢(shì)在哪里,為什么具備這些優(yōu)勢(shì)。

npm

npm 全稱,Node Package Manager node包管理工具

執(zhí)行npm install 之后。npm 幫我們下載對(duì)應(yīng)的依賴包并解壓到本地緩存,然后構(gòu)造node_modules目錄結(jié)構(gòu),寫入依賴文件,對(duì)應(yīng)的node_modules內(nèi)部結(jié)構(gòu)也經(jīng)歷了幾個(gè)版本的變化。

npm v1/v2 嵌套依賴

最開始其實(shí)沒有注重npm包的管理,只是簡單的嵌套依賴,這種方式層級(jí)依賴結(jié)構(gòu)清晰

但是隨著npm包的增多,項(xiàng)目的迭代擴(kuò)展,重復(fù)包越下載越多,造成了空間浪費(fèi),導(dǎo)致前端本地項(xiàng)目node_modules 動(dòng)輒上百M(fèi)

在業(yè)務(wù)開發(fā)中,安裝幾個(gè)項(xiàng)目,項(xiàng)目體積好幾G,對(duì)使用者們極其不友好。

入下圖所示,依賴包C 在AB 中都被引用了, 被重復(fù)下載了兩次,其實(shí)是兩個(gè)完全相同的東西。

從我們現(xiàn)在的角度看,完全沒有必要。

image

npm v3 扁平化

node_modules體積過大,嵌套過深

npm 團(tuán)隊(duì)也意識(shí)到這個(gè)問題,通過扁平化的方式,將子依賴安裝到了主依賴所在項(xiàng)目中,以減少依賴嵌套太深,和重復(fù)下載安裝的問題。

如下圖所示,A 的依賴項(xiàng)C 被提升到了頂層,如果后續(xù)有安裝包,也依賴C,會(huì)去上一級(jí)的node_modules查找,如果有相同版本的包,則不會(huì)再去重復(fù)下載,直接從上一層拿到需要的依賴包C

說明:為什么自己的node_modules沒有C,也能在上層訪問到C呢?

require 尋找第三方包,會(huì)每層級(jí)依次去尋找node_modules,所以即便本層級(jí)沒有node_moudles,上層有,也能找到

image

扁平化方式解決了相同包重復(fù)安裝的問題,也一定程度上解決了依賴層級(jí)太深的問題。

為什么說是一定程度上?

因?yàn)槿缟蠄D所示,B 依賴的C v2.0.0,并沒有提升,依然是嵌套依賴。

因?yàn)樵趦蓚€(gè)依賴包 C 的版本號(hào)不一致,只能保證一個(gè)在頂層,上圖所示C v1.0.0 被提升了,v2.0.0 沒有被提升,后續(xù)v2.0.0 還是會(huì)被重復(fù)下載,所以當(dāng)出現(xiàn)多重依賴時(shí),依然會(huì)出現(xiàn)重復(fù)安裝的問題。\

而且這個(gè)提升的順序,也不是根據(jù)使用量優(yōu)先提升,而是根據(jù)先來先服務(wù)原則,先安裝的先提升。這會(huì)導(dǎo)致不確定性問題,隨著項(xiàng)目迭代,npm i 之后得到的node_modules目錄結(jié)構(gòu),有可能不一樣。

與此同時(shí),我們把C,提升到了頂層,即使項(xiàng)目package.json,沒有聲明過C,但是也可以在項(xiàng)目中引用到C,這就是幽靈依賴問題。


可以說 npm v3 在解決嵌套依賴,重復(fù)安裝問題的同時(shí),又帶來了新的問題。

npm v5 lock

npm v5 借鑒yarn的思想,新增了 package-lock.json。

該文件里面記錄了package.json依賴的模塊,以及模塊的子依賴。并且給每個(gè)依賴標(biāo)明了版本、獲取地址和驗(yàn)證模塊完整性哈希值。

通過package-lock.json,保障了依賴包安裝的確定性與兼容性,使得每次安裝都會(huì)出現(xiàn)相同的結(jié)果。

這個(gè)就解決了不確定性的問題

package-lock.json文件字段說明

image
  • name:項(xiàng)目的名稱;
  • version:項(xiàng)目的版本;
  • lockfileVersion:lock文件的版本;
  • requires:使用requires來跟蹤模塊的依賴關(guān)系;
  • dependencies:項(xiàng)目的依賴
    • version表示實(shí)際安裝的版本;
    • resolved用來記錄下載的地址,registry倉庫中的位置;
    • requires記錄當(dāng)前模塊的依賴;
    • integrity用來從緩存中獲取索引,再通過索引去獲取壓縮包文件

npm install 過程

至此我們也可以順帶總結(jié)一下npm install的全過程

npm install先檢測是有package-lock.json文件:

  • 沒有package-lock.json文件

    • 分析依賴關(guān)系,這是因?yàn)槲覀兛赡馨鼤?huì)依賴其他的包,并且多個(gè)包之間會(huì)產(chǎn)生相同依賴的情況;
    • 從registry倉庫中下載壓縮包(如果我們?cè)O(shè)置了鏡像,那么會(huì)從鏡像服務(wù)器下載壓縮包);
    • 獲取到壓縮包后會(huì)對(duì)壓縮包進(jìn)行緩存(從npm5開始有的, npm config get cache 可以查看地址)
    • 將壓縮包解壓到項(xiàng)目的node_modules文件夾中
  • 有package-lock.json文件

    • 檢測lock中包的版本是否和package.json中一致

      • 不一致,那么會(huì)重新構(gòu)建依賴關(guān)系,直接會(huì)走上面的流程;
    • 一致的情況下,會(huì)去優(yōu)先查找緩存

      • 緩存沒有找到,從registry倉庫下載,直接走上面流程;
      • 命中緩存會(huì)獲取緩存中的壓縮文件
    • 將壓縮文件解壓到node_modules文件夾中;

image

pnpm

綜上,基于npm扁平化node_modules的結(jié)構(gòu)下,雖然解決了依賴嵌套、重復(fù)安裝的問題,但多重依賴和幽靈依賴并沒有好的解決方式。

pnpm出現(xiàn)就是為了解決現(xiàn)在npm 存在的問題,正如官網(wǎng)pnpm 所形容自己的是一款速度快,節(jié)省磁盤空間的軟件包管理器。

image

前置知識(shí) 軟鏈接&硬鏈接

簡單理解

硬鏈接就是多個(gè)文件名指向了同一個(gè)文件,這多個(gè)文件互為硬鏈接。

像是JS 中的兩個(gè)相同的對(duì)象,a 和b 的真實(shí)內(nèi)容指向堆中同一個(gè)地址,修改一個(gè),同時(shí)改變,一榮俱榮,一損俱損。刪除一個(gè),并不影響另一個(gè)。

let a = {test:1} 
let b = a
a.test = 2
console.log(b) // {test:2}

軟鏈接就是快捷方式,是一個(gè)單獨(dú)文件。

就像我們電腦桌面上的快捷方式,大小只有幾字節(jié),指向源文件,點(diǎn)擊快捷方式,其實(shí)執(zhí)行的就是源文件。

專業(yè)理解

在 Linux 的文件系統(tǒng)中,保存在磁盤分區(qū)中的文件不管是什么類型都給它分配一個(gè)編號(hào),稱為索引節(jié)點(diǎn)號(hào)(Inode Index)。A 是 B 的硬鏈接(A 和 B 都是文件名)則 A 文件中的 inode 節(jié)點(diǎn)號(hào)與 B 文件的 inode 節(jié)點(diǎn)號(hào)相同,即一個(gè) inode 節(jié)點(diǎn)對(duì)應(yīng)兩個(gè)不同的文件名,兩個(gè)文件名指向同一個(gè)文件,

軟硬鏈接 是linux 中解決文件的共享使用問題的兩個(gè)方式,目的也是為了節(jié)省磁盤空間。

大家可以去網(wǎng)上找找專業(yè)教程,或者報(bào)名山月的linux 訓(xùn)練營,這里就不展開說了。

node_modules的層級(jí)結(jié)構(gòu)

比如某項(xiàng)目中,package.json里聲明了A和B,

A的package.json 里聲明了C v1.0.0,B的package.json 里聲明了C v2.0.0

image

進(jìn)行pnpm i 之后,node_modules的層級(jí)結(jié)構(gòu)如下

雙鍵頭代表硬鏈接

單箭頭代表軟鏈接

node_modules
|_ A -> .pnpm/A@1.0.0/node_modules/A
|_ B -> .pnpm/B@1.0.0/node_modules/B
|_ .pnpm
  |_ A@1.0.0
    |_ node_modules
      |_ A => pnpm/store/A 
      |_ C -> ../../C@1.0.0/node_modules/C
  |_ B@1.0.0
    |_ node_modules
      |_ B => pnpm/store/B 
      |_ C -> ../../C@2.0.0/node_modules/C
  |_ C@1.0.0
    |_ node_modules
      |_ C => pnpm/store/C 
  |_ C@2.0.0
    |_ node_modules
      |_ C => pnpm/store/C

以A 包為例,A的目錄下并沒有node_modules,是一個(gè)軟鏈接,真正的文件位于 .pnpm/A@1.0.0/node_modules/A 并硬鏈接到全局store中。

A 和 B 是我們?cè)陧?xiàng)目package.json中聲明的依賴包,node_modules除了A,B 沒有其他包,說明不是扁平化結(jié)構(gòu)。也就不存在 幽靈依賴的問題。

.pnpm 中存放著所有的包。最終硬鏈接指向指向全局pnpm 倉庫里的store目錄下。

也就是說,我們所有的包,最終都以硬鏈接的形式,最終都在全局 pnpm/store 中,可以使得不同的項(xiàng)目從全局 store 尋找到同一個(gè)依賴,大大節(jié)省了磁盤空間。

如果上面這個(gè)文件列表不夠直觀,大家也可以看我參考官網(wǎng)畫的結(jié)構(gòu)圖

image

生產(chǎn)驗(yàn)證

全局安裝 brew install pnpm

以我自己基于vue-cli封裝的一個(gè)移動(dòng)端項(xiàng)目vue-template為例

github 地址如下
基于vue-cli二次封裝的移動(dòng)端框架,vue3 +vue-cli4 + webpack5 + 多入口打包 + 自動(dòng)生成項(xiàng)目模版 + pinia + 數(shù)據(jù)持久化 + 路由動(dòng)畫 + axios二次封裝

npm i 之后

查看node_modules 體積 293M

du -h -s node_modules
293M node_modules

查看package-lcok.json中重復(fù)文件,以postcss為例,一眼就看到了兩個(gè)版本的postcss版本,

查看node_modules只有一個(gè)版本的postcss 包會(huì)被提升,其他版本的就會(huì)被重復(fù)下載

image

pnpm i 之后

查看node_modules 體積 251M

du -h -s node_modules\
251M node_modules

切換到node_modules目錄下,查看所有文件信息

cd node_modules
ls -alh

以axios庫為例,只有37B,只是一個(gè)快捷方式,axios 軟鏈接指向 .pnpm/axios@0.26.1/node_modules/axios

image

切換到.pnpm 目錄下,查看所有文件信息

cd .pnpm
ls -alh

我們看到postcss三個(gè)版本文件夾,說明現(xiàn)在項(xiàng)目里依賴三個(gè)版本的postcss

image

切換到postcss@7.0.39目錄,查看文件信息

cd postcss@7.0.39/node_modules/postcss
stat -x package.json

值得關(guān)注的屬性有兩個(gè),一個(gè)是Links,表示硬鏈接個(gè)數(shù),一個(gè)是Inode

image

我們可以通過Inode 去查詢所有的硬鏈接

find . -inum 8177610

可以看到,在全局Library/pnpm/store/下對(duì)應(yīng)的文件目錄
4條記錄 也對(duì)應(yīng)了 links:4

image

對(duì)比

對(duì)比發(fā)現(xiàn),當(dāng)一個(gè)項(xiàng)目時(shí),兩者差距不大。

舉一個(gè)極端的例子,當(dāng)有10個(gè)相同項(xiàng)目時(shí),npm 的node_modules 將達(dá)到2930M,將近3個(gè)G,而pnpm 依舊能保持 全局253M的體積,此時(shí)優(yōu)勢(shì)已經(jīng)很明顯了。

我們?cè)跇I(yè)務(wù)開發(fā)時(shí),其實(shí)一般都通用的模版,所以項(xiàng)目的依賴基本上一致,我覺得pnpm還是非常好的。

全局安裝目錄 pnpm-store的目錄結(jié)構(gòu)

pnpm
└── store
    └── v3
        └── files
            ├── 00
              - cd3e571524c095736
              - 02a74db92f0368580
            ├── 01
            ├── 02

上圖是我們?nèi)帜夸浵聀npm 的目錄結(jié)構(gòu)。

我們?cè)谌帜夸浝锎娣诺牟皇莕pm 包的源碼,而是hash文件,這里采用了基于文件內(nèi)容尋址方案。

簡單來說就是文件內(nèi)容被加密成了64位hash值,hash值都是唯一的,如果文件內(nèi)容不變,hash 值也不會(huì)變。

這個(gè)非常適合npm的安裝包,一般來說,依賴包的更新都是向下兼容的,兩個(gè)版本的包差別只是部分,而我們使用hash存儲(chǔ),會(huì)根據(jù)文件內(nèi)容變化,只會(huì)存儲(chǔ)變化的部分,相同的部分,生成的hash不會(huì)變,只存儲(chǔ)一份就夠了,一定程度上,也節(jié)省了磁盤空間。

pnpm 弊端

調(diào)試問題

所有項(xiàng)目引用的包都在全局一個(gè)地方,如果想對(duì)某個(gè)包進(jìn)行調(diào)試,其他項(xiàng)目正好引用了,本地運(yùn)行也會(huì)收到影響。

兼容問題

symlink 即軟連接的方式可能會(huì)在 windows 存在一些兼容的問題,但是針對(duì)這個(gè)問題,pnpm 也提供了對(duì)應(yīng)的解決方案:在 win 系統(tǒng)上使用一個(gè)叫做 junctions 的特性來替代軟連接,這個(gè)方案在 window 上的兼容性要好于 symlink

我沒有windows電腦,沒有實(shí)驗(yàn)過,這條是從官網(wǎng)挪過來了。

我理解的是window下也是可以使用的,pnpm 已經(jīng)幫我們做了兼容,只是沒有使用軟鏈接的方案。

pnpm 常見問題

為什么使用硬鏈接? 為什么不直接創(chuàng)建到全局存儲(chǔ)的軟鏈接?

這個(gè)問題非常復(fù)雜,說來話長,我一點(diǎn)點(diǎn)分析,我花了很多功夫在這個(gè)問題上,目前也沒有答案,和大家分享一下我的調(diào)研結(jié)果。

首先,pnpm 官網(wǎng)如此解釋

直接軟鏈至全局存儲(chǔ)與 Node 的 --preserve-symlinks 標(biāo)志一起使用是可行的,但是,該方法附帶了個(gè)自己的問題,因此我們決定使用硬鏈接。

大意就是可以做,但我們不想,因?yàn)闀?huì)引發(fā)新的問題。

require 直接引入軟鏈接

軟鏈接的文件中,使用require 直接引用的包會(huì)報(bào)錯(cuò),軟鏈接會(huì)從文件原始位置開始查找依賴。

我們希望的是軟鏈可以將其他地方的目錄增加到依賴查找路徑中。

有興趣可以去看github 關(guān)于軟鏈接引用報(bào)錯(cuò)的討論,這時(shí)已經(jīng)有人提出使用硬鏈接https://github.com/nodejs/node/issues/3402

我們實(shí)驗(yàn)一下

如下圖,建立兩個(gè)文件夾a,b

image

a/index.js中寫入,b中安裝qs庫

const test = require('qs')
console.log(test)

b 中建立index.js的軟鏈接index-s.js

執(zhí)行node index-s.js 發(fā)現(xiàn)找不到模塊

因?yàn)檐涙溄又械膔equire軟鏈接會(huì)從文件原始位置開始查找依賴,a中沒有node_modules,直接報(bào)錯(cuò)了,但是如果是硬鏈接則不存在這樣的問題

image

--preserve-symlinks

最后node官方,增加了--preserve-symlinks來專門處理軟鏈接的引用路徑問題。

Node.js 有這樣一個(gè)選項(xiàng):–preserve-symlinks,可以設(shè)置成按照軟鏈所在的位置查找依賴。

新的問題

–preserve-symlinks 會(huì)引發(fā)新的問題,但是我查閱了github 的issues,有好幾百條的討論,沒有看到有詳細(xì)解釋清楚這個(gè)問題的,我現(xiàn)在大概的理解就是node官方對(duì)軟鏈接支持的不夠好,即使提出了–preserve-symlinks,也有問題,所以pnpm團(tuán)隊(duì)不用了。

有興趣可以看看老外們的討論

https://github.com/npm/npm/issues/15133

后來,我在node.js 中文文檔里找到著這么一句,但是自己沒有驗(yàn)證

使用 --preserve-symlinks 會(huì)有其他方面的影響。 比如,如果符號(hào)連接的原生模塊在依賴樹里來自超過一個(gè)位置,它們會(huì)加載失敗。 (Node.js 會(huì)將它們視為兩個(gè)獨(dú)立的模塊,且會(huì)試圖多次加載模塊,造成拋出異常。)

https://www.nodeapp.cn/cli.html#cli_preserve_symlinks

image

最終作者拋棄了這個(gè)方案

image

總結(jié)

最后我們?cè)俜g翻譯,pnpm 官網(wǎng)的這些話

節(jié)省磁盤空間

pnpm通過hard link(硬連接) 機(jī)制,把包都存儲(chǔ)在全局的pnpm/store/目錄下。當(dāng)安裝軟件包時(shí),其包含的所有文件都會(huì)硬鏈接自此位置,而不會(huì)占用額外的硬盤空間。pnpm 對(duì)于同一個(gè)包不同的版本也僅存儲(chǔ)其增量改動(dòng)的部分。

快速

安裝包之前,如果已經(jīng)在全局安裝過,就不會(huì)被再次下載了,節(jié)省了安裝時(shí)間。隨著項(xiàng)目增多,效果會(huì)越來越明顯。

支持單體倉庫

pnpm 提供工作空間workspace能力,就是保證一個(gè)倉庫內(nèi)多個(gè)項(xiàng)目的package.json有自己生效的范圍。這個(gè)yarn npm 也支持,不算pnpm的突出點(diǎn)。我對(duì)monorepos 也沒有研究過,這塊等后續(xù)有時(shí)間了,可以對(duì)比三個(gè)工具的workspace專題討論。

嚴(yán)格

pnpm 默認(rèn)創(chuàng)建了一個(gè)非扁平化的 node_modules,因此代碼無法訪問未聲明的包,解決了npm 存在的幽靈依賴問題。

待研究的問題

  • pnpm-lock.yaml 文件里的屬性和生成過程
  • pnpm 對(duì)peerDependencies 的處理
  • 老項(xiàng)目使用yarn 或者npm 如何遷移
  • pnpm npm yarn 工作空間workspace的研究
  • Java的meavns是怎么管理依賴包的,和前端有什么區(qū)別

參考鏈接

pnpm官網(wǎng) https://pnpm.io/zh/

軟鏈、硬鏈對(duì) Node.js 包尋找的影響 https://meixg.cn/2021/01/25/ln-nodejs/#%E8%BD%AF%E9%93%BE%E4%BE%9D%E8%B5%96%E7%9B%AE%E5%BD%95

前端包管理工具 npm yarn cnpm npx https://juejin.cn/post/7102442481920425997

pnpm 有什么優(yōu)勢(shì) https://q.shanyue.tech/engineering/751.html#pnpm

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容