npm 依賴管理中一些重要的細節

前言

npm(全稱 Node Package Manager,即「node包管理器」)是Node.js預設的、用JavaScript編寫的包管理工具。雖然是Node.js中的工具,但如今更多的被用來配合前端構建工具給前端進行包管理。html

做爲一個包管理器,最重要的就是管理依賴了。對於複雜的依賴樹,npm 的處理機制和其餘的包管理器會有所不一樣,本文將會詳細介紹這些細節前端

npm2和npm3+版本對依賴的處理有所不一樣,但如今不多有使用npm3如下版本的項目了,本文中全部的介紹都是基於npm3+以上版本java

npm 依賴管理機制

npm 大致上來看,和其餘的包管理器差很少,都是包依賴包,而且用版本號來聲明這些依賴的包。node

語義化版本號

npm 中使用語義化版本來控制版本依賴包的版本,好比^~>=<之類的範圍符號,不過本文中版本號的解析方式不是重點,只須要知道若是使用範圍版本號,npm會安裝範圍內可用的最新版本
**
這裏要吐槽一下npm的文檔,光是找這個範圍版本號具體使用的版本策略,就找了好久,文檔中並無清晰的說明……最後在npm update頁面中找到了一絲介紹webpack

If app’s package.json contains:

"dependencies": {"dep1": "^1.1.1"}git

Then npm update will install dep1@1.2.2, because 1.2.2 is latest and 1.2.2 satisfies ^1.1.1.github

npm的這個範圍版本設計的理念仍是挺先進的,經過範圍版本號讓使用方能夠及時的自動更新小版本,升級後可能修復一些bug,可是隨之而來的也會有不少因更新致使的風險。畢竟版本號是人類控制的,人類控制就有可能出現失誤,好比一個修訂版本號的更新中刪除了某些api,致使沒法兼容web

我的看來,這種範圍版本號的包管理機制,是弊大於利的,風險太高。若是在服務端場景下,什麼都沒改的狀況下就偷摸換了個(小)版本,極可能會出現一些嚴重的事故。通常來講,任何改動都須要通過測試,尤爲是這種依賴包升級,是個挺有風險的事情。若是是那種通用基礎包的風險就更大了,引用的地方過多,極可能出現一些不兼容的狀況。npm

依賴樹和傳遞依賴

npm 會默認會將傳遞依賴的包用flat的形式,也安裝至node_modules的根目錄,好比有一個模塊A,他依賴了模塊B:
npm_dependency_managenent.svg**json

版本衝突

如今增長一個模塊C,C也依賴B,可是C依賴了B的高版本V2.0,此時npm的處理就有點不同了;因爲C依賴的B模塊版本和A依賴的B版本不兼容,npm 會先將A模塊依賴的B1.0安裝至根目錄,而後將C依賴的B2.0安裝至C本身的node_modules中,以下圖所示
npm_dependency_managenent_2.svg
目錄結構

|————mod-A@1.0
|————mod-B@1.0
|————mod-C@1.0
    |————mod-B@2.0

對於版本不兼容的依賴樹,npm的處理是先檢查是否版本兼容,若是版本兼容就不重複安裝,若是和以前的的傳遞依賴包版本不兼容,那麼就將該依賴包安裝至當前引用的包的node_modules下
**
npm 的包版本衝突解決方案雖然帶來了包文件的 冗餘,但能夠很好的解決衝突問題

這種版本衝突解決機制真的很完美嗎?

歷來面的介紹能夠看出,當出現版本不兼容時,npm會將依賴的包安裝至當前包的node_modules下,有點submodule的意思,但也不是真的萬無一失,仍是有可能出現因爲多版本共存致使的衝突。

仍是拿上面的A/B/C三個依賴模塊來舉例,好比B v1.0中向window對象註冊了一個屬性,B v2.0也向window中註冊了一個屬性,因爲B v1.0和v2.0差距很大,雖然註冊的是同一個對象,但屬性和其函數差距很大,當一個頁面同時引入A和C模塊時,B v1.0和B v2.0都會加載,可能會出現一些意外的錯誤。對於使用者來講是不能接受的
npm_dependency_conflict.svg

上面這個例子可能還不是很恰當,由於註冊window這件事原本就有必定風險。如今設想另外一種常見的場景,好比有在Angular(2)中,兩個基於Angular的組件依賴了不一樣的Angular(Core)大版本,那麼當一個頁面同時使用兩個組件,而且兩個組件須要在當前頁面進行交互時,好比賦值或者函數調用之類,就很容易出現上圖中的問題。

這種問題在Java生態中的包管理雖然也有,但形式會有所不一樣:

在Maven中(Java生態的包管理工具),雖然依賴是樹狀結構的,但構建後的結果實際上是平面(flat)的的。若是出現多個版本的jar包,運行時通常會將全部jar包都加載;不過因爲JAVA中ClassLoader的parent delegate機制,一樣的Class只會被加載一次,下N個Jar包內的的同名類(包名+類名)會被忽略,這樣的好處是簡單,若是出現版本衝突也清晰可見,衝突問題須要使用者自行處理。

Maven Build對包(傳遞)依賴多版本的處理,以下圖所示:
npm&maven_dependency_management.svg
npm 對於這種可能出現的版本衝突問題,也提供了一個解決辦法:peerDependencies

peerDependencies

peerDependencies和maven中的provide scope很像,當一個依賴模塊X定義在peerDependencies中而不是devDependencies或dependencies中時,依賴該模塊的項目就不會自動下載該依賴。

項目中須要直接或間接的聲明符合該版本的依賴,直接依賴是指直接在devDependencies或dependencies中聲明,間接依賴是指當前項目依賴的其餘模塊依賴了X符合版本範圍的模塊,若是兩者都不知足,在npm install時會出現一個告警,好比:

npm WARN hidash@0.2.0 requires a peer of lodash@~1.3.1 but none is installed. You must install peer dependencies yourself.

npm & webpack

如今不少項目都會使用webpack來做爲項目的構建工具,可是和java中的maven 不一樣,webpack和npm是兩套獨立的工具,構建和包管理是分開的

也就是說,哪怕npm將衝突包做爲「submodule」的形式安裝在當前包內,可是webpack可不必定認

好比上面ABC三個模塊的例子,若是A模塊的代碼中import BObj from B mod,那麼webpack構建以後,會讓A引用哪個B版本呢?v1.0 仍是 v2.0?

這個場景至關複雜,本文就不介紹了,有一篇文章詳細介紹了webpack下的處理方式和測試場景:《Finding and fixing duplicates in webpack with Inspectpack》

總結

npm 包管理的設計理念雖然很好,但不適合全部的場景,好比這種submodule的模式拿到java裏就不可行,並且submodule的模式仍是有必定的風險,只是風險下降了。一旦有多個依賴的代碼在一個頁面同時工做或交互,就很容易出問題。

不管是什麼包管理工具,最安全的作法仍是避免重複。在增長新依賴或是新建項目後,使用一些依賴分析檢查工具檢測一遍,修復重複/衝突的依賴。

參考

相關文章
相關標籤/搜索