什么是PNPM?
Fast, disk space efficient package manager
本質上他是一個包管理工具,和npm/yarn沒有區別,主要優勢在于
- 包安裝速度極快
- 磁盤空間利用效率高
使用方法:
npm i pnpm -g
優勢
一、速度快
二、高效利用磁盤空間
硬鏈接(Hard Link)
硬連接指通過索引節點來進行連接。在Linux的文件系統中,保存在磁盤分區中的文件不管是什么類型都給它分配一個編號,稱為索引節點號(Inode Index)。在Linux中,多個文件名指向同一索引節點是存在的。一般這種連接就是硬連接。硬連接的作用是允許一個文件擁有多個有效路徑名,這樣用戶就可以建立硬連接到重要文件,以防止“誤刪”的功能。其原因如上所述,因為對應該目錄的索引節點有一個以上的連接。只刪除一個連接并不影響索引節點本身和其它的連接,只有當最后一個連接被刪除后,文件的數據塊及目錄的連接才會被釋放。也就是說,文件真正刪除的條件是與之相關的所有硬連接文件均被刪除。
軟連接(Symbolic Link)
另外一種連接稱之為符號連接,也叫軟連接。軟鏈接文件有類似于Windows的快捷方式。它實際上是一個特殊的文件。在符號連接中,文件實際上是一個文本文件,其中包含的有另一文件的位置信息。
pnpm 內部使用基于內容尋址
的文件系統來存儲磁盤上所有的文件:
- 不會重復安裝同一個包。使用
npm/yarn
的時候,如果100個包依賴lodash
,那么就可能安裝了100次lodash
,磁盤中就有100個地方寫入了這部分代碼。但是pnpm
會只在一個地方寫入這部分代碼,后面使用會直接使用hard link
- 即使一個包的不同版本,pnpm 也會極大程度地復用之前版本的代碼。舉個例子,比如 lodash 有 100 個文件,更新版本之后多了一個文件,那么磁盤當中并不會重新寫入 101 個文件,而是保留原來的 100 個文件的
hardlink
,僅僅寫入那一個新增的文件
。
三、支持monorepo
monorepo 的宗旨就是用一個 git 倉庫來管理多個子項目,所有的子項目都存放在根目錄的packages
目錄下,那么一個子項目就代表一個package
。
monorepo 管理工具lerna
項目參考:babel
四、安全
之前在使用 npm/yarn 的時候,由于 node_module 的扁平結構,如果 A 依賴 B, B 依賴 C,那么 A 當中是可以直接使用 C 的,但問題是 A 當中并沒有聲明 C 這個依賴。因此會出現這種非法訪問的情況。 但 pnpm 自創了一套依賴管理方式,很好地解決了這個問題,保證了安全性。
依賴管理
pnpm 會在全局的 store 目錄里存儲項目 node_modules
文件的 hard links
。因為這樣一個機制,導致每次安裝依賴的時候,如果是個相同的依賴,有好多項目都用到這個依賴,那么這個依賴實際上最優情況(即版本相同)只用安裝一次。
回歸一下 node_modules 結構歷史:
第一階段:npm@3 之前版本
node_modules
└─ foo
├─ index.js
├─ package.json
└─ node_modules
└─ bar
├─ index.js
└─ package.json
- 依賴樹層級太深,會導致 Windows 上的目錄路徑過長問題
- 相同包在不同的依賴項中需要時,會存在多個相同副本
第二階段:npm@3 版本,扁平化處理
所有的依賴都被拍平到node_modules
目錄下,不再有很深層次的嵌套關系。這樣在安裝新的包時,根據 node require 機制,會不停往上級的node_modules
當中去找,如果找到相同版本的包就不會重新安裝,解決了大量包重復安裝的問題,而且依賴層級也不會太深。
node_modules
├─ foo
| ├─ index.js
| └─ package.json
└─ bar
├─ index.js
└─ package.json
但還是存在一些問題
- 依賴結構的不確定性。
- 扁平化算法本身的復雜性很高,耗時較長。
- 項目中仍然可以非法訪問沒有聲明過依賴的包
這就是為什么會產生依賴結構的不確定
問題,也是 lock 文件
誕生的原因,無論是package-lock.json
(npm 5.x才出現)還是yarn.lock
,都是為了保證 install 之后都產生確定的node_modules
結構。
盡管如此,npm/yarn 本身還是存在扁平化算法復雜
和package 非法訪問
的問題,影響性能和安全。
第三階段:pnpm
由于扁平化算法的極其復雜,以及會存在多項目間相同依賴副本的情況。pnpm 在嘗試解決這些問題時,放棄了扁平化處理 node_modules 的方式。而是采用 硬鏈+軟鏈 方式。
node_modules
├─ .pnpm
| ├─ foo@1.0.0/node_modules/foo
| | └─ index.js
| └─ bar@2.0.0/node_modules/bar
├─ foo -> .pnpm/foo@1.0.0/node_modules/foo
└─ bar -> .pnpm/bar@2.0.0/node_modules/bar
node_modules 根目錄中的包只是一個符號鏈接。require('foo')
將執行 node_modules/.pnpm/foo@1.0.0/node_modules/foo/indexjs
中的文件(這里是硬鏈接),而不是 node_modules/foo/index.js
中的文件。
這種布局結構的一大好處是只有真正在依賴項中(package.json dependences)的包才能訪問
舉個??
安裝一個express
依賴,會在 node_modules 中形成這樣兩個目錄結構:
node_modules/express/...
node_modules/.pnpm/express@4.17.1/node_modules/xxx
其中第一個路徑是 nodejs 正常尋找路徑會去找的一個目錄,如果去查看這個目錄下的內容,會發現里面連個 node_modules
文件都沒有:
? express
? lib
History.md
index.js
LICENSE
package.json
Readme.md
實際上這個文件只是個軟連接,它會形成一個到第二個目錄的一個軟連接(類似于軟件的快捷方式),這樣 node 在找路徑的時候,最終會找到 .pnpm 這個目錄下的內容。
其中這個 .pnpm
是個虛擬磁盤目錄,然后 express 這個依賴的一些依賴會被平鋪到 .pnpm/express@4.17.1/node_modules/
這個目錄下面,這樣保證了依賴能夠 require 到,同時也不會形成很深的依賴層級。
在保證了 nodejs 能找到依賴路徑的基礎上,同時也很大程度上保證了依賴能很好的被放在一起。
目前不適用的場景
前面有提到關于 pnpm 的主要問題在于 symlink(軟鏈接)在一些場景下會存在兼容的問題,可以參考作者在 nodejs 那邊開的一個 discussion:https://github.com/nodejs/node/discussions/37509
在里面作者提到了目前 nodejs 軟連接不能適用的一些場景,希望 nodejs 能提供一種 link 方式而不是使用軟連接,同時也提到了 pnpm 目前因為軟連接而不能使用的場景:
- Electron 應用無法使用 pnpm
- 部署在 lambda 上的應用無法使用 pnpm
一些 nodejs 基礎庫不支持 symlink 的情況導致使用 pnpm 無法正常工作,不過這些庫在迭代更新之后也會支持這一特性。
總結
pnpm 方式的實現精髓
- 通過軟鏈的形式,使得 require 可以正常引用;同時對非真正依賴的項目做隔離(避免引用依賴的混亂)
-
.pnpm
的存在避免了循環引用和層級過深的問題(都在其第一層) - 硬鏈使得不同項目相同依賴只存在一個副本,減少磁盤空間
未來會做的一些事情
脫離 nodejs
具體可以參考 https://github.com/pnpm/pnpm/discussions/3434
- 安裝 pnpm 的, 可以基本上脫離掉 nodejs 這個 runtime 去進行安裝使用。
- 可以通過 pnpm 來使用不同版本的 nodejs 來去做依賴安裝,類似于 nvm 提供的功能。
目前該特性其實已經到了 beta 版本,可以參考 https://www.npmjs.com/package/@pnpm/beta 這個包。管理不同版本的 nodejs 功能可以參考 env 這個子命令: https://pnpm.io/cli/env
使用 rust 寫一些模塊
具體可以看 https://github.com/pnpm/pnpm/discussions/3419 這個 discussion 討論的內容,大概就是作者希望給 pnpm 的一些子命令提供一些 rust 的 cli wrapper 來做提升性能使用。