今天研究了一下 pnpm
的機制,發現它確實很強大,甚至可以說對 yarn
和 npm
形成了降維打擊 。
我們從包管理工具的發展歷史,一起看下到底好在哪裡?
npm2
在 npm 3.0 版本之前,專案的 node_modules
會呈現出巢狀結構,也就是說,我安裝的依賴、依賴的依賴、依賴的依賴的依賴...,都是遞迴巢狀的
node_modules
├─ express
│ ├─ index.js
│ ├─ package.json
│ └─ node_modules
│ ├─ accepts
│ │ ├─ index.js
│ │ ├─ package.json
│ │ └─ node_modules
│ │ ├─ mime-types
| | | └─ node_modules
| | | └─ mime-db
| │ └─ negotiator
│ ├─ array-flatten
│ ├─ ...
│ └─ ...
└─ A
├─ index.js
├─ package.json
└─ node_modules
└─ accepts
├─ index.js
├─ package.json
└─ node_modules
├─ mime-types
| └─ node_modules
| └─ mime-db
└─ negotiator
設計缺陷
這種巢狀依賴樹的設計確實存在幾個嚴重的問題
- 路徑過長問題: 由於包的巢狀結構 ,
node_modules
的目錄結構可能會變得非常深,甚至可能會超出系統路徑長度上限 ,畢竟 windows 系統的檔案路徑預設最多支援 256 個字元 - 磁碟空間浪費: 多個包之間難免會有公共的依賴,公共依賴會被多次安裝在不同的包目錄下,導致磁碟空間被大量浪費 。比如上面
express
和 A 都依賴了accepts
,它就被安裝了兩次 - 安裝速度慢:由於依賴包之間的巢狀結構,
npm
在安裝包時需要多次處理和下載相同的包,導致安裝速度變慢,尤其是在依賴關係複雜的專案中
當時 npm 還沒解決這些問題, 社群便推出了新的解決方案 ,就是 yarn。 它引入了一種新的依賴管理方式——扁平化依賴。
看到 yarn 的成功,npm 在 3.0 版本中也引入了類似的扁平化依賴結構
yarn
yarn 的主要改進之一就是透過扁平化依賴結構來解決巢狀依賴樹的問題,具體來說
鋪平,yarn 儘量將所有依賴包安裝在專案的頂層 node_modules
目錄下,而不是巢狀在各自的 node_modules
目錄中。
這樣一來,減少了目錄的深度,避免了路徑過長的問題 ,也儘可能避免了依賴被多次重複安裝的問題
我們可以在 yarn-example 看到整個目錄,全部鋪平在了頂層 node_modules
目錄下,展開下面的包大部分是沒有二層 node_modules
的
然而,有些依賴包還是會在自己的目錄下有一個 node_modules
資料夾,出現巢狀的情況,例如 yarn-example 下的http-errors
依賴包就有自己的 node_modules
,原因是:
當一個專案的多個依賴包需要同一個庫的不同版本時,yarn 只能將一個版本的庫提升到頂層 node_modules
目錄中。 對於需要這個庫其他版本的依賴,yarn 仍然需要在這些依賴包的目錄下建立一個巢狀的 node_modules
來存放不同版本的包
比如,包 A 依賴於 lodash@4.0.0
,而包 B 依賴於 lodash@3.0.0
。由於這兩個版本的 lodash
不能合併,yarn
會將 lodash@4.0.0
提升到頂層 node_modules
,而 lodash@3.0.0
則被巢狀在包 B 的 node_modules
目錄下。
幽靈依賴
雖然 yarn 和 npm 都採用了扁平化的方案來解決依賴巢狀的問題,但這種方案本身也有一些缺陷,其中幽靈依賴是一個主要問題。
幽靈依賴,也就是你明明沒有在 package.json
檔案中宣告的依賴項,但在專案程式碼裡卻可以 require
進來
這個也很容易理解,因為依賴的依賴被扁平化安裝在頂層 node_modules
中,所以我們能訪問到依賴的依賴
但是這樣是有隱患的,因為沒有顯式依賴,未來某個時候這些包可能會因為某些原因消失(例如新版本庫不再引用這個包了,然後我們更新了庫),就會引發程式碼執行錯誤
浪費磁碟空間
而且還有一個問題,就是上面提到的依賴包有多個版本的時候,只會提升一個,那其餘版本的包不還是複製了很多次麼,依然有浪費磁碟空間的問題
那社群有沒有解決這倆問題的思路呢? pnpm 就是其中最成功的一個
pnpm
pnpm 透過全域性儲存和符號連結機制從根源上解決了依賴重複安裝和路徑長度問題,同時也避免了扁平化依賴結構帶來的幽靈依賴問題
pnpm 的優勢概括來說就是“快、準、狠”:
- 快:安裝速度快
- 準:安裝過的依賴會準確複用快取,甚至包版本升級帶來的變化都只 diff,絕不浪費一點空間
- 狠:直接廢掉了幽靈依賴
執行 npm add express
,我們可以在 pnpm-example 看到整個目錄,由於只安裝了 express
,那 node_modules
下就只有 express
那麼所有的(次級)依賴去哪了呢? binggo,在node_modules/.pnpm/
目錄下,.pnpm/
以平鋪的形式儲存著所有的包
三層定址
- 所有 npm 包都安裝在全域性目錄
~/.pnpm-store/v3/files
下,同一版本的包僅儲存一份內容,甚至不同版本的包也僅儲存 diff 內容。 - 頂層
node_modules
下有.pnpm
目錄以打平結構管理每個版本包的原始碼內容,以硬連結方式指向 pnpm-store 中的檔案地址。 - 每個專案
node_modules
下安裝的包以軟連結方式將內容指向node_modules/.pnpm
中的包。
所以每個包的尋找都要經過三層結構:node_modules/package-a
> 軟連結 node_modules/.pnpm/package-a@1.0.0/node_modules/package-a
> 硬連結 ~/.pnpm-store/v3/files/00/xxxxxx
。
這就是 pnpm 的實現原理。官方給了一張原理圖,可以搭配食用
前面說過,npm 包都被安裝在全域性 pnpm store
,預設情況下,會建立多個儲存(每個驅動器(磁碟機代號)一個),並在專案所在磁碟機代號的根目錄
所以,同一個磁碟機代號下的不同專案,都可以共用同一個全域性 pnpm store
,絕絕子啊👏,大大節省了磁碟空間,提高了安裝速度
軟硬連結
也就是說,所有的依賴都是從全域性 store 硬連線到了 node_modules/.pnpm
下,然後之間透過軟連結來相互依賴。
那麼,這裡的軟連線、硬連結到底是什麼東西?
硬連結是指向磁碟上原始檔案所在的同一位置 (直接指向相同的資料塊)
軟連線可以理解為新建一個檔案,它包含一個指向另一個檔案或目錄的路徑 (指向目標路徑)
.npmrc
shamefully-hoist
,預設 false
- false:
node_modules
下只能看到直接依賴的套件,次級依賴在node_modules/.pnpm
目錄下;無法訪問其他子包區域性安裝的依賴項,例如,vue-dome2 安裝的 lodash,vue-dome1 是訪問不到的 - true:將所有套件都拉昇到
node_modules
目錄下,能訪問到其他子包區域性安裝的依賴項,例如,vue-dome2 安裝的 lodash,vue-dome1 是能訪問到的
// .npmrc
# pnpm 配置
shamefully-hoist=false
總結
npm2 的巢狀結構: 每個依賴項都會有自己的 node_modules
目錄,導致了依賴被重複安裝,嚴重浪費了磁碟空間💣;在依賴層級比較深的專案中,甚至會超出 windows 系統的檔案路徑長度💣
npm3+ 和 Yarn 的扁平化策略: 儘量將所有依賴包安裝在專案的頂層 node_modules
目錄下,解決了 npm2
巢狀依賴的問題。但是該方案有一個重大缺陷就是“幽靈依賴”💣;而且依賴包有多個版本時,只會提升一個,那其餘版本依然會被重複安裝,還是有浪費磁碟空間的問題💣
pnpm全域性儲存和符號連結機制: 結合軟硬鏈和三層定址,解決了依賴被重複安裝的問題,更加變態的是,同一磁碟機代號下的不同專案都可以共用一個全域性 pnpm store
。節省了磁碟空間,並且根本不存在“幽靈依賴”,安裝速度還賊快💪💪💪
參考文件
weekly/前沿技術/253.精讀《pnpm》
pnpm 是憑什麼對 npm 和 yarn 降維打擊的)
平鋪的結構不是 node_modules 的唯一實現方式 | pnpm中文網