本文作者對比了當前主流的包管理工具npm、yarn、pnpm之間的區別,並提出了合適的使用建議,以下為譯文:
NPM
npm是Node.js能夠如此成功的主要原因之一。npm團隊做了很多的工作,以確保npm保持向後相容,並在不同的環境中保持一致。
npm是圍繞著語義版本控制(semver)的思想而設計的,下面是從他們的網站摘抄過來的:
給定一個版本號:主版本號.次版本號.補丁版本號, 以下這三種情況需要增加相應的版本號:
- 主版本號: 當API發生改變,並與之前的版本不相容的時候
- 次版本號: 當增加了功能,但是向後相容的時候
- 補丁版本號: 當做了向後相容的缺陷修復的時候
npm使用一個名為package.json的檔案,使用者可以通過npm install –save命令把專案裡所有的依賴項儲存在這個檔案裡。
例如,執行npm install –save lodash會將以下幾行新增到package.json檔案中。
"dependencies": {
"lodash": "^4.17.4"
}
請注意,在版本號lodash之前有個^字元。這個字元告訴npm,安裝主版本等於4的任意一個版本即可。所以如果我現在執行npm進行安裝,npm將安裝lodash的主版本為4的最新版,可能是 lodash@4.25.5(@是npm約定用來確定包名的指定版本的)。你可以在此處檢視所有支援的字元:https://docs.npmjs.com/misc/semver。
理論上,次版本號的變化並不會影響向後相容性。因此,安裝最新版的依賴庫應該是能正常工作的,而且能引入自4.17.4版本以後的重要錯誤和安全方面的修復。
但是,另一方面,即使不同的開發人員使用了相同的package.json檔案,在他們自己的機器上也可能會安裝同一個庫的不同種版本,這樣就會存在潛在的難以除錯的錯誤和“在我的電腦上…”的情形。
大多數npm庫都嚴重依賴於其他npm庫,這會導致巢狀依賴關係,並增加無法匹配相應版本的機率。
雖然可以通過npm config set save-exact true命令關閉在版本號前面使用^的預設行為,但這個只會影響頂級依賴關係。由於每個依賴的庫都有自己的package.json檔案,而在它們自己的依賴關係前面可能會有^符號,所以無法通過package.json檔案為巢狀依賴的內容提供保證。
為了解決這個問題,npm提供了shrinkwrap命令。此命令將生成一個npm-shrinkwrap.json檔案,為所有庫和所有巢狀依賴的庫記錄確切的版本。
然而,即使存在npm-shrinkwrap.json這個檔案,npm也只會鎖定庫的版本,而不是庫的內容。即便npm現在也能阻止使用者多次重複釋出庫的同一版本,但是npm管理員仍然具有強制更新某些庫的權力。
這是引用自shrinkwrap文件的內容:
如果你希望鎖定包中的特定位元組,比如是為了保證能正確地重新部署或構建,那麼你應該在原始碼控制中檢查依賴關係,或者採取一些其他的機制來校驗內容,而不是靠校驗版本。
npm 2會安裝每一個包所依賴的所有依賴項。如果我們有這麼一個專案,它依賴專案A,專案A依賴專案B,專案B依賴專案C,那麼依賴樹將如下所示:
node_modules
- package-A
-- node_modules
--- package-B
----- node_modules
------ package-C
-------- some-really-really-really-long-file-name-in-package-c.js
這個結構可能會很長。這對於基於Unix的作業系統來說只不過是一個小煩惱,但對於Windows來說卻是個破壞性的東西,因為有很多程式無法處理超過260個字元的檔案路徑名。
npm 3採用了扁平依賴關係樹來解決這個問題,所以我們的3個專案結構現在看起來如下所示:
node_modules
- package-A
- package-B
- package-C
-- some-file-name-in-package-c.js
這樣,一個原來很長的檔案路徑名就從./node_modules/package-A/node_modules/package-B/node-modules/some-file-name-in-package-c.js變成了/node_modules/some-file-name-in-package-c.js。
你可以在這裡閱讀到更多有關NPM 3依賴解析的工作原理。
這種方法的缺點是,npm必須首先遍歷所有的專案依賴關係,然後再決定如何生成扁平的node_modules目錄結構。npm必須為所有使用到的模組構建一個完整的依賴關係樹,這是一個耗時的操作,是npm安裝速度慢的一個很重要的原因。
由於我沒有詳細瞭解npm的變化,所以我想當然的以為每次執行npm install命令時,NPM都得從網際網路上下載所有內容。
但是,我錯了,npm是有本地快取的,它儲存了已經下載的每個版本的壓縮包。本地快取的內容可以通過npm cache ls命令進行檢視。本地快取的設計有助於減少安裝時間。
總而言之,npm是一個成熟、穩定、並且有趣的包管理器。
Yarn
Yarn釋出於2016年10月,並在Github上迅速擁有了2.4萬個Star。而npm只有1.2萬個Star。這個專案由一些高階開發人員維護,包括了Sebastian McKenzie(Babel.js)和Yehuda Katz(Ember.js、Rust、Bundler等)。
從我搜集到的情況來看,Yarn一開始的主要目標是解決上一節中描述的由於語義版本控制而導致的npm安裝的不確定性問題。雖然可以使用npm shrinkwrap來實現可預測的依賴關係樹,但它並不是預設選項,而是取決於所有的開發人員知道並且啟用這個選項。
Yarn採取了不同的做法。每個yarn安裝都會生成一個類似於npm-shrinkwrap.json的yarn.lock檔案,而且它是預設建立的。除了常規資訊之外,yarn.lock檔案還包含要安裝的內容的校驗和,以確保使用的庫的版本相同。
由於yarn是嶄新的經過重新設計的npm客戶端,它能讓開發人員並行化處理所有必須的操作,並新增了一些其他改進,這使得執行速度得到了顯著的提升,整個安裝時間也變得更少。我估計,速度提升是yarn受歡迎的主要原因。
像npm一樣,yarn使用本地快取。與npm不同的是,yarn無需網際網路連線就能安裝本地快取的依賴項,它提供了離線模式。這個功能在2012年的npm專案中就被提出來過,但一直沒有實現。
yarn還提供了一些其他改進,例如,它允許合併專案中使用到的所有的包的許可證,這一點讓人很高興。
一個有趣的事情是,yarn文件的態度開始針對npm發生改變,因為yarn專案變得流行起來。
最開始的yarn公告是這麼介紹yarn的安裝的:
*最簡單的入門方法是執行:
npm install -g yarn
yarn*
現在的yarn安裝頁面是這麼說的:
注意:通常情況下不建議通過npm進行安裝。npm安裝是非確定性的,程式包沒有簽名,並且npm除了做了基本的SHA1雜湊之外不執行任何完整性檢查,這給安裝系統程式帶來了安全風險。
基於這些原因,強烈建議你通過最適合於你的作業系統的安裝方法來安裝yarn。
以這種速度發展下去的話,如果yarn要宣佈他們自己的registry,讓開發者慢慢淘汰npm的話,我們一點都不會感到驚訝。
看起來似乎要感謝yarn,npm終於意識到他們需要更加關注一些大家強烈要求的問題了。當我在稽核我之前提到的強烈要求的“離線”功能時,我注意到這個需求正在被積極地修復之中。
pnpm
正如我所提到的,在pnpm的作者Zoltan Kochan發表了“為什麼要用pnpm?”之後,我才知道pnpm。
我不會介紹太多的細節(因為這篇文章已經發布很久了),但是你可以檢視我的最初的帖子來尋找更多的內容,同時在Twitter上加入討論。
但是
我想指出的是,pnpm執行起來非常的快,甚至超過了npm和yarn。
為什麼這麼快呢? 因為它採用了一種巧妙的方法,利用硬連結和符號連結來避免複製所有本地快取原始檔,這是yarn的最大的效能弱點之一。
使用連結並不容易,會帶來一堆問題需要考慮。
正如Sebastian在Twitter上指出的那樣,他最初是打算在yarn中使用符號連結的,但是由於其他一些原因放棄了它。
同時,正如在Github上擁有2000多個Star那樣,pnpm能夠為許多人所用。
此外,截至2017年3月,它繼承了yarn的所有優點,包括離線模式和確定性安裝。
總結
我認為yarn和pnpm的開發人員做了一個驚人的工作。我個人喜歡的是確定性安裝,因為我喜歡控制,我不喜歡驚喜。
無論這場競爭的結果是什麼,我很感謝yarn在npm的腳下點了一把火,提供了另外一個選擇。
我確信yarn是一個更安全的選擇,但是pnpm可能是一些測試用例的更好的選擇。例如,它可以在執行大量整合測試並希望儘可能快地安裝依賴關係的中小型團隊中發揮作用。
最後,我認為,npm仍然提供了一個非常有用的解決方案,支援大量的測試用例。大多數開發人員使用原始npm客戶端仍然可以做得很好。