pnpm才是前端工程化項目的未來
前言?
相信小伙伴們都接觸過npm/yarn?,這兩種包管理工具想必是大家工作中用的最多的包管理工具,npm?作為node?官方的包管理工具,它是隨著node的誕生一起出現在大家的視野中,而yarn?的出現則是為了解決npm?帶來的諸多問題,雖然yarn?提高了依賴包的安裝速度與使用體驗,但它依舊沒有解決npm?的依賴重復安裝等致命問題。「pnpm」的出現完美解決了依賴包重復安裝的問題,并且實現了yarn帶來的所有優秀體驗,所以說「pnpm才是前端工程化項目的未來」。
npm 與 yarn 存在的問題?
早期的npm
在npm@3之前,node_modules?結構可以說是整潔?、可預測的,因為當時的依賴結構是這樣的:
node_modules
└─ 依賴A
├─ index.js
├─ package.json
└─ node_modules
└─ 依賴B
├─ index.js
└─ package.json
└─ 依賴C
├─ index.js
├─ package.json
└─ node_modules
└─ 依賴B
├─ index.js
└─ package.json
每個依賴下面都維護著自己的node_modules,這樣看起來確實非常整潔,但同時也帶來一些較為嚴重的問題:
- 依賴包重復安裝
- 依賴層級過多
- 模塊實例無法共享
依賴包重復安裝
從上面的依賴結構我們可以看出,依賴A與依賴C同時引用了依賴B,此時的依賴B會被下載兩次。此刻我們想想要是某一個依賴被引用了n次,那么它就需要被下載n次。(此時心里是不是在想,怎么會有如此坑的設計)
依賴層級過多
我們再來看另外一種依賴結構:
node_modules
└─ 依賴A
├─ index.js
├─ package.json
└─ node_modules
└─ 依賴B
├─ index.js
├─ package.json
└─ node_modules
└─ 依賴C
├─ index.js
├─ package.json
└─ node_modules
└─ 依賴D
├─ index.js
└─ package.json
這種依賴層級少還能接受,要是依賴層級多了,這樣一層一層嵌套下去,就像一個依賴地獄,不利于維護。
npm@3與yarn
為了解決上述問題,npm3?與yarn?都選擇了扁平化結構,也就是說現在我們看到的node_modules里面的結構不再有依賴嵌套了,都是如下依賴結構:
node_modules
└─ 依賴A
├─ index.js
├─ package.json
└─ node_modules
└─ 依賴C
├─ index.js
├─ package.json
└─ node_modules
└─ 依賴B
├─ index.js
├─ package.json
└─ node_modules
node_modules下所有的依賴都會平鋪到同一層級。由于require尋找包的機制,如果A和C都依賴了B,那么A和C在自己的node_modules中未找到依賴C的時候會向上尋找,并最終在與他們同級的node_modules中找到依賴包C。這樣就不會出現重復下載的情況。而且依賴層級嵌套也不會太深。因為沒有重復的下載,所有的A和C都會尋找并依賴于同一個B包。自然也就解決了實例無法共享數據的問題。
由于這個扁平化結構的特點,想必大家都遇到了這樣的體驗,自己明明就只安裝了一個依賴包,打開node_modules文件夾一看,里面卻有一大堆。
這種扁平化結構雖然是解決了之前的嵌套問題,但同時也帶來了另外一些問題:
- 依賴結構的不確定性
- 扁平化算法的復雜度增加
- 項目中仍然可以非法訪問沒有聲明過的依賴包(幽靈依賴)
依賴結構的不確定性
這個怎么理解,為什么會產生這種問題呢?我們來仔細想想,加入有如下一種依賴結構:
A包與B包同時依賴了C包的不同版本,由于同一目錄下不能出現兩個同名文件,所以這種情況下同一層級只能存在一個版本的包,另外一個版本還是要被嵌套依賴。
那么問題又來了,既然是要一個扁平化一個嵌套,那么這時候是如何確定哪一個扁平化哪一個嵌套的呢?
這兩種結構都有可能,準確點說哪個版本的包被提升,取決于包的安裝順序!
這就是為什么會產生依賴結構的不確定?問題,也是 lock 文件?誕生的原因,無論是package-lock.json?(npm 5.x 才出現)還是yarn.lock?,都是為了保證 install 之后都產生確定的node_modules結構。
盡管如此,npm/yarn 本身還是存在扁平化算法復雜?和package 非法訪問的問題,影響性能和安全。
pnpm?
前面說了那么多的npm?與yarn的缺點,現在再來看看pnpm是如何解決這些尷尬問題的。
什么是pnpm
快速的,節省磁盤空間的包管理工具
就這么簡單,說白了它跟npm?與yarn沒有區別,都是包管理工具。但它的獨特之處在于:
- 包安裝速度極快
- 磁盤空間利用非常高效
特性
安裝包速度快
從上圖可以看出,pnpm的包安裝速度明顯快于其它包管理工具。那么它為什么會比其它包管理工具快呢?
我們來可以來看一下各自的安裝流程
- npm/yarn
- resolving:首先他們會解析依賴樹,決定要fetch哪些安裝包。
- fetching:安裝去fetch依賴的tar包。這個階段可以同時下載多個,來增加速度。
- wrting:然后解壓包,根據文件構建出真正的依賴樹,這個階段需要大量文件IO操作。
- pnpm
上圖是pnpm的安裝流程,可以看到針對每個包的三個流程都是平行的,所以速度會快很多。當然pnpm會多一個階段,就是通過鏈接組織起真正的依賴樹目錄結構。
磁盤空間利用非常高效
pnpm 內部使用基于內容尋址的文件系統來存儲磁盤上所有的文件,這個文件系統出色的地方在于:
- 不會重復安裝同一個包。用 npm/yarn 的時候,如果 100 個項目都依賴 lodash,那么 lodash 很可能就被安裝了 100 次,磁盤中就有 100 個地方寫入了這部分代碼。但在使用 pnpm 只會安裝一次,磁盤中只有一個地方寫入,后面再次使用都會直接使用hardlink。
- 即使一個包的不同版本,pnpm 也會極大程度地復用之前版本的代碼。舉個例子,比如 lodash 有 100 個文件,更新版本之后多了一個文件,那么磁盤當中并不會重新寫入 101 個文件,而是保留原來的 100 個文件的hardlink,僅僅寫入那一個新增的文件。
支持monorepo
pnpm 與 npm/yarn 另外一個很大的不同就是支持了 monorepo,pnpm內置了對monorepo的支持,只需在工作空間的根目錄創建pnpm-workspace.yaml和.npmrc配置文件,同時還支持多種配置,相比較lerna和yarn workspace,pnpm解決monorepo的同時,也解決了傳統方案引入的問題。
monorepo 的宗旨就是用一個 git 倉庫來管理多個子項目,所有的子項目都存放在根目錄的packages目錄下,那么一個子項目就代表一個package。
依賴管理
pnpm使用的是npm version 2.x類似的嵌套結構,同時使用.pnpm 以平鋪的形式儲存著所有的包。然后使用Store + Links和文件資源進行關聯。簡單說pnpm把會包下載到一個公共目錄,如果某個依賴在 sotre 目錄中存在了話,那么就會直接從 store 目錄里面去 hard-link,避免了二次安裝帶來的時間消耗,如果依賴在 store 目錄里面不存在的話,就會去下載一次。通過Store + hard link的方式,使得項目中不存在NPM依賴地獄問題,從而完美解決了npm3+和yarn中的包重復問題。
我們分別用npm與pnpm來安裝vite對比看一下
npm | pnpm |
所有依賴包平鋪在node_modules目錄,包括直接依賴包以及其他次級依賴包 | node_modules?目錄下只有.pnpm和直接依賴包,沒有其他次級依賴包 |
沒有符號鏈接(軟鏈接) | 直接依賴包的后面有符號鏈接(軟鏈接)的標識 |
pnpm安裝的vite 所有的依賴都軟鏈至了 node_modules/.pnpm/ 中的對應目錄。把 vite 的依賴放置在同一級別避免了循環的軟鏈。
軟鏈接和硬鏈接機制
pnpm 是通過 hardlink 在全局里面搞個 store 目錄來存儲 node_modules 依賴里面的 hard link 地址,然后在引用依賴的時候則是通過 symlink 去找到對應虛擬磁盤目錄下(.pnpm 目錄)的依賴地址。
這兩者結合在一起工作之后,假如有一個項目依賴了 A@1.0.0 和 B@1.0.0 ,那么最后的 node_modules 結構呈現出來的依賴結構可能會是這樣的:
node_modules
└── A
└── B
└── .pnpm
├── A@1.0.0
│ └── node_modules
│ └── A -> <store>/A
│ ├── index.js
│ └── package.json
└── B@1.0.0
└── node_modules
└── B -> <store>/B
├── index.js
└── package.json
node_modules 中的 A 和 B 兩個目錄會軟連接到 .pnpm 這個目錄下的真實依賴中,而這些真實依賴則是通過 hard link 存儲到全局的 store 目錄中。
store
?pnpm?下載的依賴全部都存儲到?store?中去了,?store?是?pnpm?在硬盤上的公共存儲空間。
pnpm?的store?在Mac/linux中默認會設置到{home dir}>/.pnpm-store/v3;windows下會設置到當前盤符的根目錄下。使用名為 .pnpm-store的文件夾名稱。
項目中所有.pnpm/依賴名@版本號/node_modules/?下的軟連接都會連接到pnpm的store中去。