筆者之前在開發模塊分析工具,使用npm list命令時遇到 UNMET PEER DEPENDENCY 這個問題,在探究解決方法的時候對npm的包管理機制有了很多新的認識,分享一下過程中的思考。
UNMET PEER DEPENDENCY 是什麼 ?
你在使用npm list命令的時候,可能遇到過下面這種npm 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源創計劃”,歡迎正在閱讀的你也加入,一起分享。