引言
對于包管理器,不同語言其實都有自己的包管理器,比如: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
雖然通過扁平化政策,确實減少了部分嵌套依賴太深和重複安裝的問題,為什麼說是解決部分問題呢?看下圖:
可以看到,項目中同時依賴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結構如下:
通過yarn安裝依賴後,生成的yarn.lock和node_modules結構如下:
對比可以看到:
- yarn.lcok檔案中,所有的依賴項描述都是扁平化的,結構簡單明了;
- 在yarn.lock中,相同名稱版本不同的依賴包,如果semver範圍相同會被合并,同時會存在多個版本描述;
- yarn 在生成 yarn.lock 檔案時,使用更嚴格的版本解析算法,會确切地記錄每個依賴項的版本。這意味着無論何時重新安裝依賴,yarn 都會使用相同的版本,進而確定了依賴版本的一緻性;
semver規範
SemVer 是指語義版本規範(Semantic Versioning),用來約定包版本格式。它由三部分組成:主版本号、次版本号和修訂版本号。
- 主版本号(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 保持所有依賴關系平坦,但使用符号連結将它們分組在一起。
性能對比
pnpm的局限性
以下是來自官網的描述:
- npm-shrinkwrap.json 和 package-lock.json 被忽略。與 pnpm 不同,npm可以多次安裝相同的 name@version ,并且具有不同的依賴項組合。npm 的鎖檔案旨在反映平鋪的 node_modules 布局,但是,由于 pnpm 預設建立隔離布局,它無法由 npm 的鎖檔案格式反映出來。但是,如果您希望将鎖定檔案轉換為 pnpm 的格式,請看 pnpm import (https://pnpm.io/zh/cli/import)。
- 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):
作者:宋永傑
來源-微信公衆号:Goodme前端團隊
出處:https://mp.weixin.qq.com/s/KfGy90i8qoSlIQVbQ7bU8Q