天天看點

探究前端包管理工具:npm、yarn 和pnpm

作者:閃念基因

引言

對于包管理器,不同語言其實都有自己的包管理器,比如:Python/Rust有自己的包管理器(pip/cargo),還有如rpm、maven等。

同樣在現代前端開發中,bower、npm、yarn、cnpm、pnpm等各種包管理器,簡化了資源引用的依賴關系,提升了我們的開發效率。本文将從包管理器的發展史和當下主流的三種工具:npm、yarn和pnpm來做一個全方位的分析和對比,探讨各自優點和适用場景。

遠古時期

nodejs誕生之前,我們想要引用一些三方資源庫,比如jquery,經常使用以下方式:

  • 遠端下載下傳zip壓縮包,解壓以後将資源檔案放入項目中,并進行引用。
  • 通過cdn的方式,将資源連結以script标簽引入到html中。

随之就會出現版本管理混亂、項目檔案過大、cdn資源失效、依賴更新等各種問題。

随着nodejs的爆火和子產品化概念的誕生,npm出現了,最初npm隻是用于服務端nodejs的包管理器,随着前端社群的不斷發展,npm也使用在了用戶端開發中。

那麼當包管理工具出現後,是怎麼逐漸解決上述問題呢?這就得從它的發展史聊起了

包管理工具發展史

npm

npm v3之前

2011年7月,npm釋出了1.0版本。當時的node_modules檔案夾是什麼樣子呢?

node_modules
└─ foo
   ├─ index.js
   ├─ package.json
   └─ node_modules
      └─ bar
         ├─ index.js
         └─ package.json
           

存在的問題

  • node_modules檔案夾體積過大,比如當多個項目同時引用lodash,就會在node_modules多次安裝lodash,很快就會把計算機的磁盤占滿,不得不經常通過 rm -rf node_modules 來删除node_modules。
  • 嵌套層級過深,隻有當找到不依賴任何包的葉子節點,才會停止,會導緻路徑過長,在windows會出現删除node_modules失敗問題。
  • 安裝速度很慢,有目錄嵌套的原因,也有安裝邏輯的問題,按照隊列下載下傳,這就會導緻同一個時刻,隻有一個子產品在下載下傳、解析、安裝。

npm V3

為了解決上述問題,npm團隊認真思考了node_modules的結構,并提出了扁平化的政策,就是把嵌套過深的層級,通過扁平化的方式,将依賴包進行提升,使嵌套層級盡可能的變少。在npm v3階段,node_modules的結構如下:

node_modules
├─ foo
|  ├─ index.js
|  └─ package.json
└─ bar
   ├─ index.js
   └─ package.json
           

雖然通過扁平化政策,确實減少了部分嵌套依賴太深和重複安裝的問題,為什麼說是解決部分問題呢?看下圖:

探究前端包管理工具:npm、yarn 和pnpm

可以看到,項目中同時依賴b1.0.0和b2.0.0,隻有b1.0.0提升安裝在頂層,b2.0.0照樣還是會重複安裝。實際上b2.0.0也有可能被提升安裝在頂層,b1.0.0重複安裝。并且決定這個提升順序遵循的是先到先安裝的政策,是以存在很多的不确定性。

存在的問題

  • 沒有徹底解決重複安裝的問題;
  • 存在幽靈依賴問題,比如在安裝時把[email protected]提升到了頂層,但是在package.json中并沒有聲明,項目中照樣可以引用[email protected];
  • 不支援離線緩存模式,安裝速度慢;

yarn

yarn的出現,可以說是從根本上解決了npm存在的很多問題,比如資源一緻性、安裝速度慢等問題。

資源一緻性解決方案:版本鎖定

yarn的出現,我覺着最大的貢獻就是推出了yarn.lock,來解決依賴版本錯亂的問題,npm在一年後的npm@5也跟上yarn的腳步,推出了package-lock.json。

npm V5和yarn在處理扁平化的方式上的差別:

// 在一個項目中存在如下依賴:
node_modules
├─ htmlparser2@^3.10.1
|  ├─ entities@^1.1.1
└─ dom-serializer@^0.2.2
|  ├─ entities@^2.0.0
└─ entities@^2.1.0 
           

通過npm install安裝依賴後,生成的package-lock.json和node_modules結構如下:

探究前端包管理工具:npm、yarn 和pnpm

通過yarn安裝依賴後,生成的yarn.lock和node_modules結構如下:

探究前端包管理工具:npm、yarn 和pnpm

對比可以看到:

  • yarn.lcok檔案中,所有的依賴項描述都是扁平化的,結構簡單明了;
  • 在yarn.lock中,相同名稱版本不同的依賴包,如果semver範圍相同會被合并,同時會存在多個版本描述;
  • yarn 在生成 yarn.lock 檔案時,使用更嚴格的版本解析算法,會确切地記錄每個依賴項的版本。這意味着無論何時重新安裝依賴,yarn 都會使用相同的版本,進而確定了依賴版本的一緻性;

semver規範

SemVer 是指語義版本規範(Semantic Versioning),用來約定包版本格式。它由三部分組成:主版本号、次版本号和修訂版本号。

探究前端包管理工具:npm、yarn 和pnpm
  • 主版本号(MAJOR): 當更新API無法進行向下相容,會破壞現有代碼功能的時候,必須更新主版本号。
  • 次版本号(MINOR): 添加了向下相容的功能,可以更新次版本号。此時意味着增加了新功能,但是不影響現有功能使用。
  • 修訂版本号(PATCH): 當進行向下相容的bug修複,可以更新修訂版本号。這意味着新版本隻是對之前版本中的錯誤進行了修複,沒有添加新的功能,且與之前的版本相容。
  • 預發版本号和版本建構号(TAG): 通常使用連接配接符“-”和“+”來連接配接,比如:2.1.3-beat.1+build3.2

yarn是否把npm拍死在沙灘上?

實際上,yarn本質上還是在下載下傳npm包,隻是針對npm v3中的痛點,針對性的做了優化:

緩存機制:

  • yarn使用一個全局的緩存目錄來存儲所有依賴項,而npm使用分散的緩存目錄結構。這樣使得yarn更加易于管理和維護。- yarn擁有離線模式,當你在指令行敲下yarn install 時,會首先嘗試使用本地緩存,當你之前已經緩存過這些依賴項,那麼在離線模式下也能安裝。
  • 并行安裝:yarn在設計之初就考慮到了并行安裝依賴,預設使用多線程來下載下傳和安裝依賴包,使得安裝速度更快。
  • 版本鎖定更加穩定:如上分析,yarn.lock的檔案更加扁平化和準确,能夠最大限度避免多個版本依賴問題。

社群裡有人針對yarn和npm的性能做了對比(來源于:github.com/appleboy/npm-vs-yarn):

npm installnpm ciyarninstall without cache (without node_modules)3m3m1minstall with cache (without node_modules)1m18s30sinstall with cache (with node_modules)54s21s2sinstall without internet (with node_modules)--2s

pnpm

為什麼被稱為最先進的包管理工具?

pnpm項目建立的初衷:

  • 節省磁盤空間
  • 提高安裝速度
  • 建立一個非扁平的node_modules目錄

節省磁盤空間

在使用npm進行依賴安裝的時候,不同項目有相同依賴的時候,都會被重複安裝。在使用 pnpm 時,依賴會被存儲在内容可尋址的存儲倉庫(store)中,采用store + hardLink的方式:

  • 當項目中引用了某個依賴項的不同版本,那麼pnpm在安裝的時候,隻會将不同版本中的差異檔案添加到store中。比如我們項目中的依賴的新版本隻更改了其中1個檔案,那麼pnpm update的時候隻會向store中更新這1個檔案,而不會更改整個依賴封包件。
  • 所有的依賴封包件都存儲在全局的store目錄下,當某個項目在安裝依賴時,會通過硬連結的方式将依賴資源連結到項目中,而不會再次重複安裝依賴包,不占用額外的磁盤空間。

提高安裝速度

上面提到過,pnpm采用store + hardLink的方式進行依賴的管理和安裝。

  • 當一個項目中有多個相同的依賴包時,pnpm隻需要下載下傳一次,然後通過hardLink的方式進行不同項目中的引用。
  • pnpm使用并行方式安裝依賴項,可以同時下載下傳多個依賴項,進一步提升安裝速度。

建立一個非扁平的node_modules目錄

使用 npm 或 Yarn 安裝依賴項時,所有的包都被提升到子產品目錄的根目錄。這樣就導緻了一個問題,源碼可以直接通路和修改依賴,而不是作為隻讀的項目依賴。

首先來看下pnpm是怎麼解決嵌套依賴問題的:

-> - a symlink (or junction on Windows)

node_modules
├─ foo -> .registry.npmjs.org/foo/1.0.0/node_modules/foo
└─ .registry.npmjs.org
   ├─ foo/1.0.0/node_modules
   |  ├─ bar -> ../../bar/2.0.0/node_modules/bar
   |  └─ foo
   |     ├─ index.js
   |     └─ package.json
   └─ bar/2.0.0/node_modules
      └─ bar
         ├─ index.js
         └─ package.json
           

在pnpm 建立的node_modules檔案夾中,所有包都有自己的依賴項分組在一起,但目錄樹永遠不會像 npm@2 那樣深。pnpm 保持所有依賴關系平坦,但使用符号連結将它們分組在一起。

性能對比

探究前端包管理工具:npm、yarn 和pnpm

pnpm的局限性

以下是來自官網的描述:

  1. npm-shrinkwrap.json 和 package-lock.json 被忽略。與 pnpm 不同,npm可以多次安裝相同的 name@version ,并且具有不同的依賴項組合。npm 的鎖檔案旨在反映平鋪的 node_modules 布局,但是,由于 pnpm 預設建立隔離布局,它無法由 npm 的鎖檔案格式反映出來。但是,如果您希望将鎖定檔案轉換為 pnpm 的格式,請看 pnpm import (https://pnpm.io/zh/cli/import)。
  2. Binstubs(在 node_modules/.bin中的檔案)總是 shell 檔案,而不是指向 JS 檔案的符号連結。建立 shell 檔案是為了幫助支援插件的 CLI 的程式在特殊的 node_modules 結構中能夠正确地找到它們的插件。這是很少有的問題,如果您希望檔案是 JS 檔案,請直接引用原始檔案,如 #736 (https://github.com/pnpm/pnpm/issues/736) 所示。

總結

npm、yarn和pnpm都是當下十分優秀的包管理工具,具體選擇哪個,還是要根據團隊項目情況和個人喜好來決定。npm 是 Node.js 生态系統的一部分,yarn 提供了更快的依賴項安裝和鎖定檔案功能,而 pnpm 則專注于減少磁盤空間的使用和安裝時間。

附上三者在一些功能上的比較(https://pnpm.io/zh/feature-comparison):

探究前端包管理工具:npm、yarn 和pnpm

作者:宋永傑

來源-微信公衆号:Goodme前端團隊

出處:https://mp.weixin.qq.com/s/KfGy90i8qoSlIQVbQ7bU8Q

繼續閱讀