這個煩人的 NPM 安裝依賴報錯,暗藏了幾個知識點

筆者之前在開發模塊分析工具,使用npm list命令時遇到 UNMET PEER DEPENDENCY 這個問題,在探究解決方法的時候對npm的包管理機制有了很多新的認識,分享一下過程中的思考。

UNMET PEER DEPENDENCY 是什麼 ?

你在使用npm list命令的時候,可能遇到過下面這種npm ERR:

UNMET PEER DEPENDENCY ERR

當你去檢查依賴的樹狀結果,你會發現每一行npm ERR都有對應一行這樣的結果:

UNMET PEER DEPENDENCY,翻譯過來還挺難理解的,意思是說父依賴缺少了這個依賴的對等版本。拿上面的例子來說,就是eslint-config-imweb的0.2.10版本,需要版本在4.9.0到5.0.0這個區間(左閉右開)的eslint包。

你可能會發現上面例子中,imweb的eslint規則是從airbnb風格繼承而來的,所以這個版本的eslint其實是airbnb這個包所缺失的。缺失的這個版本的eslint包沒有被安裝,它在依賴樹中所在的層級尚不明確,因此在eslint-config-imweb、eslint-config-airbnb下都出現了UNMET PEER DEPENDENCY的提示。

按理說,執行過npm install,我的node_modules就已經有一個eslint了,怎麼會提示我缺了eslint。其實這正是模塊分析工具的需求痛點,項目下的某個包,往往會在依賴樹的不同節點,存在多種版本。在深究原因之前,我們需要了解平時常見的版本號規則,以及npm在install的時候是如何進行依賴管理的。

依賴版本管理規則

我們開發者在發佈自己的npm包時,當然是力求功能穩定,往往會在package.json的dependencies字段對相關依賴設定不同程度的約束:

"dependencies": {
   "signale""1.4.0",
   "figlet""*",
   "react""16.x",
   "table""~5.4.6",
   "yargs""^14.0.0"
}

上面的這些版本號表示,都是基於SemVer規範而來的。它是由Github起草的一個具有指導意義的,統一的版本號表示規則。實際上就是Semantic Version(語義化版本)的縮寫。

SemVer規範官網:https://semver.org/

像前面三個包的形式很容易理解:

"signale""1.4.0": 固定版本號
"figlet""*": 任意版本(>=0.0.0
"react""16.x": 匹配主要版本(>=16.0.0 <17.0.0
"react""16.3.x": 匹配主要版本和次要版本(>=16.3.0 <16.4.0

^和~則比較特別,它們分別可以做到上面第三條規則和第四條規則的效果(最高版本爲最新版本),同時又兼容了主版本號/次版本號爲0的情況:

~: 當安裝依賴時獲取到有新版本時,安裝到 x.y.z 中 z 的最新的版本。即保持主版本號、次版本號不變的情況下,保持修訂號的最新版本。
^: 當安裝依賴時獲取到有新版本時,安裝到 x.y.z 中 y 和 z 都爲最新版本。 即保持主版本號不變的情況下,保持次版本號、修訂版本號爲最新版本。

當主版本號爲 0 的情況,會被認爲是一個不穩定版本,情況與上面不同:
主版本號和次版本號都爲 0: ^0.0.z、~0.0.z 都被當作固定版本,安裝依賴時均不會發生變化。
主版本號爲 0: ^0.y.z 表現和 ~0.y.z 相同,只保持修訂號爲最新版本。

發佈包的時候,我們也需要嚴格按SemVer規範來指定版本號,可以用semver這個npm包來幫助我們對版本號做一些比較。

semver文檔:https://github.com/npm/node-semver

  • 安裝
npm install semver
  • 判斷版本號是否符合規範,返回解析後符合規範的版本號
semver.valid('1.2.3') // '1.2.3'
semver.valid('a.b.c') // null
  • 一些其他用法
semver.clean('  =v1.2.3   ') // '1.2.3'
semver.satisfies('1.2.3''1.x || >=2.5.0 || 5.0.0 - 7.2.3') // true
semver.minVersion('>=1.0.0') // '1.0.0'

npm install 的時候,間接依賴呈現怎樣的結構 ?

在理解了版本號規則之後,我們可以開始慢慢窺探npm依賴管理背後的問題了。開發者在publish一個npm包之後,或多或少要約束某些包的版本,防止相關依賴的更新,造成功能的變化,尤其是在相關依賴還沒有經過完善的測試的情況下。比如說,我發佈了一個A包,裏面依賴了lodash的^2.2.0:

# node_modules/A/package.json
"dependencies": {
   "lodash""^2.2.0"
}

在某個項目中,我使用到了A包:

# project/package.json
"dependencies": {
   "A""^1.0.0"
}

對於項目—>A包->lodash這樣一條簡單的間接依賴鏈路,似乎沒有看出太大問題,只要A包的開發者足夠信任lodash的測試和發佈環節,A包的功能不會出太多岔子。我們嘗試npm install之後,依賴樹大概會是這樣子的:

`-- [email protected]
  `-- [email protected]

顯然lodash有着更新的版本,但A包並沒使用到,它的package.json寫死了低版本。假如現在我們的項目又引入了其他的依賴,比如說一個B包,人家用的lodash是最新的( ^4.17.20)。

# project/package.json
"dependencies": {
   "B""^4.3.2",
   "A""^1.0.0"
}

再次嘗試npm install,依賴樹是這樣子的:

+-- [email protected]
+-- [email protected]
`-- [email protected]
  `-- [email protected]

現在我們有兩條間接依賴的鏈路了,分別是項目—>A包->lodash,項目—>B包->lodash,而且lodash版本不相同,其中B包的lodash來到了和A包/B包同一層級的位置。這是 npm 3.x 版本以後 node_modules 的扁平結構。npm install時會將dependencies中位置靠前的包中的依賴,提升到上一級,這是爲了解決 npm 3.x 版本之前嵌套結構造成的模塊冗餘問題,當父級目錄的lodash能夠滿足C包、D包等依賴的lodash版本,那麼就不必重複安裝,npm install將會跳過這一過程。

罪魁禍首——peerDependencies

到這裏,我們大概已經知道npm install給我們的node_modules形成了怎樣的結構,現在可以來看看UNMET PEER DEPENDENCY是怎麼出現的了。首先來介紹一下,package.json中和依賴管理相關的幾個字段:

  • dependencies
  • devDependencies
  • optionalDependencies 可選擇的依賴包
  • peerDependencies 同等依賴
  • bundledDependencies 捆綁依賴包

UNMET PEER DEPENDENCY 的成因,就是和 peerDependencies 這個字段密切相關。這五個字段的區別和應用場景,我們可以都看一下。因爲,你可能不止會遇到UNMET PEER DEPENDENCY,還有UNMET OPTIONAL DEPENDENCY之類的,當你理解了這五個字段之後,你就知道應該如何處理UNMET DEPENDENCY系列的問題了。

1、dependencies dependencies 是無論在開發環境還是在生產環境都必須使用的依賴,是我們最常用的依賴包管理對象,例如 React,Loadsh,Axios 等,通過 npm install XXX 下載的包都會默認安裝在 dependencies 對象中,也可以使用 npm install XXX --save 下載 dependencies 中的包;

2、devDependencies devDependencies 是指可以在開發環境使用的依賴,例如 eslint,debug 等,通過 npm install packageName --save-dev 下載的包都會在 devDependencies 對象中;

dependencies 和 devDependencies 最大的區別是在打包運行時,執行 npm install 時默認會把所有依賴全部安裝,但是如果使用 npm install --production 時就只會安裝 dependencies 中的依賴,如果是 node 服務項目,就可以採用這樣的方式用於服務運行時安裝和打包,減少包大小。

3、optionalDependencies optionalDependencies 指的是可以選擇的依賴,當你希望某些依賴即使下載失敗或者沒有找到時,項目依然可以正常運行或者 npm 繼續運行的時,就可以把這些依賴放在 optionalDependencies 對象中,但是 optionalDependencies 會覆蓋 dependencies 中的同名依賴包,所以不要把一個包同時寫進這兩個對象中。

optionalDependencies 就像是我們的代碼的一種保護機制一樣,如果包存在的話就走存在的邏輯,不存在的就走不存在的邏輯。

4、peerDependencies peerDependencies 用於指定你當前的插件兼容的宿主必須要安裝的包的版本。舉個例子:我們常用的 react 組件庫 [email protected] 的 package.json 中的配置如下:

"peerDependencies": { 
"react"">=16.9.0"
"react-dom"">=16.9.0" 
}, 

假設我們創建了一個名爲 project 的項目,在此項目中我們要使用 [email protected] 這個插件,此時我們的項目就必須先安裝 React >= 16.9.0 和 React-dom >= 16.9.0 的版本。

在 npm 2 中,當我們下載 [email protected] 時,peerDependencies 中指定的依賴會隨着 [email protected] 一起被強制安裝,所以我們不需要在宿主項目的 package.json 文件中指定 peerDependencies 中的依賴,但是在 npm 3 中,不會再強制安裝 peerDependencies 中所指定的包,而是通過警告的方式來提示我們,此時就需要手動在 package.json 文件中手動添加依賴

5、bundledDependencies 這個依賴項也可以記爲 bundleDependencies,與其他幾種依賴項不同,他不是一個鍵值對的對象,而是一個數組,數組裏是包名的字符串,例如:


"name""project"
"version""1.0.0"
"bundleDependencies": [ 
  "axios",  
  "lodash" 

}

當使用 npm pack 的方式來打包時,上述的例子會生成一個 project-1.0.0.tgz 的文件,在使用了 bundledDependencies 後,打包時會把 Axios 和 Lodash 這兩個依賴一起放入包中,之後有人使用 npm install project-1.0.0.tgz 下載包時,Axios 和 Lodash 這兩個依賴也會被安裝。需要注意的是安裝之後 Axios 和 Lodash 這兩個包的信息在 dependencies 中,並且不包括版本信息。

"bundleDependencies": [ 
"axios"
"lodash" 
],
"dependencies": { 
"axios""*"
"lodash""*" 
}, 

如果我們使用常規的 npm publish 來發布的話,這個屬性是不會生效的,所以日常情況中使用的較少。

怎麼解決 UNMET PEER DEPENDENCY ?

peerDependencies儘管指定了使用某些插件時,必須要安裝的包的版本。但在不影響開發的情況下,UNMET PEER DEPENDENCY一般是可以無視的,因爲現存的很多UNMET PEER DEPENDENCY錯誤,都將已安裝的包版本指向了一個較低的版本。或者這麼說,開發者已經很久沒對peerDependencies這個字段進行更新了,像我們在描述間接依賴的時候,A包可能在peerDependencies這個字段裏面,制定我們的lodash必須安裝^2.2.0版本,可我們項目全局早就有一個4.17.20的船新版本了。

比方說,我們採用手動安裝的方式去安裝我們缺失的peerDependencies:

npm install lodash@^2.2.0

猜猜會發生什麼?這不就是49年入國軍嘛,我們項目全局的4.17.20版本被替換掉了,變成了一個2.9.9的版本了。

實際上,也確實如此,在我的項目中,遇到了stylelint-webpack-plugin的0.10.5版本,顯然它的peerDependencies是包含了stylelint,並通過警告的方式,要求我安裝一個低版本的stylelint,那我裝一下試試,看看能不能解決npm ERR:

現實往往是,不能兩全其美。我通過這種手動安裝的方式,是對項目全局的依賴進行了降級,如果有其他的子依賴也用到了stylelint的高版本,就受到了影響。

所以當出現這種問題了,其實應該儘可能要求包的發佈者去更新一下peerDependencies。當然,如果你是個強迫症,不想看到這惱人的npm ERR,可以試試下面的方法。

強迫症看這裏

1、根據我在google上搜索的一些解決方法,最簡單的方法是在系統全局安裝缺失的依賴(不需要指定版本),參考這個StackOverflow:https://stackoverflow.com/questions/35419179/unmet-peer-dependency-generator-karma-0-9-0 也就是把npm ERR這個錯誤報出來的所有包,一行全局安裝。缺點是,只能解決其中一個子依賴拋出的peerDependencies。假如還有很多子依賴,用到了更低的版本,那就用下面這種吧。

2、另一種方法是對每個npm ERR報出的包,進入到node_modules中對應包的目錄中,進行單獨的安裝,並指定版本(想想就麻煩)。

寫在最後

其實這篇文章的重點,不在於說怎麼去解決 UNMET PEER DEPENDENCY 這個問題,而是希望通過這個奇怪的現象,去理解包的依賴管理,以及npm install過程中的一些細節。在最初遇到這個問題的時候,我查閱了很多資料,最後發現僅僅是npm設計上的一些怪異之處。但在過程中其實對package.json,扁平結構和lock等設計都有了嶄新的認識。

參考文章

[1] 剖析npm包管理機制

[2] npm 依賴管理中被忽略的那些細節



順手點“在看”,每天早下班;轉發加關注,共奔小康路~

加站長好友可進微信羣,跟衆多大佬一起交流技術!

本文分享自微信公衆號 - 1024譯站(trans1024)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章