pnpm才是前端工程化專案的未來

2023-06-02 15:00:50

前言

相信小夥伴們都接觸過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

為了解決上述問題,npm3yarn都選擇了扁平化結構,也就是說現在我們看到的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

前面說了那麼多的npmyarn的缺點,現在再來看看pnpm是如何解決這些尷尬問題的。

什麼是pnpm

快速的,節省磁碟空間的包管理工具

就這麼簡單,說白了它跟npmyarn沒有區別,都是包管理工具。但它的獨特之處在於:

  • 包安裝速度極快
  • 磁碟空間利用非常高效

特性

安裝包速度快

從上圖可以看出,pnpm的包安裝速度明顯快於其它包管理工具。那麼它為什麼會比其它包管理工具快呢?

我們來可以來看一下各自的安裝流程

  • npm/yarn

  1. resolving:首先他們會解析依賴樹,決定要fetch哪些安裝包。

  2. fetching:安裝去fetch依賴的tar包。這個階段可以同時下載多個,來增加速度。

  3. 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中的包重複問題。

我們分別用npmpnpm來安裝vite對比看一下

npm pnpm
所有依賴包平鋪在node_modules目錄,包括直接依賴包以及其他次級依賴包 node_modules目錄下只有.pnpm和直接依賴包,沒有其他次級依賴包
沒有符號連結(軟連結) 直接依賴包的後面有符號連結(軟連結)的標識

pnpm安裝的vite 所有的依賴都軟鏈至了 node_modules/.pnpm/ 中的對應目錄。 把 vite 的依賴放置在同一級別避免了迴圈的軟鏈。

軟連結 和 硬連結 機制

pnpm 是通過 hardlink 在全域性裡面搞個 store 目錄來儲存 node_modules 依賴裡面的 hard link 地址,然後在參照依賴的時候則是通過 symlink 去找到對應虛擬磁碟目錄下(.pnpm 目錄)的依賴地址。

這兩者結合在一起工作之後,假如有一個專案依賴了 [email protected][email protected] ,那麼最後的 node_modules 結構呈現出來的依賴結構可能會是這樣的:

node_modules
└── A // symlink to .pnpm/[email protected]/node_modules/A
└── B // symlink to .pnpm/[email protected]/node_modules/B
└── .pnpm
    ├── [email protected]
    │   └── node_modules
    │       └── A -> <store>/A
    │           ├── index.js
    │           └── package.json
    └── [email protected]
        └── node_modules
            └── B -> <store>/B
                ├── index.js
                └── package.json

node_modules 中的 A 和 B 兩個目錄會軟連線到 .pnpm 這個目錄下的真實依賴中,而這些真實依賴則是通過 hard link 儲存到全域性的 store 目錄中。

store

pnpm下載的依賴全部都儲存到store中去了,storepnpm在硬碟上的公共儲存空間。

pnpmstore在Mac/linux中預設會設定到{home dir}>/.pnpm-store/v3;windows下會設定到當前碟符的根目錄下。使用名為 .pnpm-store的資料夾名稱。

專案中所有.pnpm/依賴名@版本號/node_modules/下的軟連線都會連線到pnpmstore中去。

原文首發地址點這裡,歡迎大家關注公眾號 「前端南玖」,如果你想進前端交流群一起學習,請點這裡

我是南玖,我們下期見!!!