npm 依賴管理中被忽略的那些細節(jié)

前言
提起 npm,大家第一個想到的應(yīng)該就是 npm install 了,但是 npm install 之后生成的 node_modules 大家有觀察過嗎?package-lock.json 文件的作用大家知道嗎?除了 dependencies 和 devDependencies,其他的依賴有什么作用呢?接下來,本文將針對 npm 中的你可能忽略的細節(jié)和大家分享一些經(jīng)驗。
npm 安裝機制
A 和 B 同時依賴 C,C 這個包會被安裝在哪里呢?C 的版本相同和版本不同時安裝會有什么差異呢?package.json 中包的前后順序?qū)τ诎惭b時有什么影響嗎?這些問題平時大家可能沒有注意過,今天我們就來一起研究一下吧。
A 和 B 同時依賴 C,這個包會被安裝在哪里呢?
假如有 A 和 B 兩個包,兩個包都依賴 C 這個包,npm 2 會依次遞歸安裝 A 和 B 兩個包及其子依賴包到 node_modules 中。執(zhí)行完畢后,我們會看到 ./node_modules 這層目錄只含有這兩個子目錄:
node_modules/
├─┬ A
│ ├── C
├─┬ B
│ └── C
如果使用 npm 3 來進行安裝的話,./node_modules 下的目錄將會包含三個子目錄:
node_modules/
├─┬ A
├─┬ B
├─┬ C
為什么會出現(xiàn)這樣的區(qū)別呢?這就要從 npm 的工作方式說起了:
npm 2 和 npm 3 模塊安裝機制的差異
雖然目前最新的 npm 版本是 npm 6,但 npm 2 到 npm 3 的版本變更中實現(xiàn)了目錄打平,與其他版本相比差別較大。因此,讓我們具體看下這兩個版本的差異。
npm install 后,npm 根據(jù) dependencies 和 devDependencies 屬性中指定的包來確定第一層依賴,npm 2 會根據(jù)第一層依賴的子依賴,遞歸安裝各個包到子依賴的 node_modules 中,直到子依賴不再依賴其他模塊。執(zhí)行完畢后,我們會看到 ./node_modules 這層目錄中包含有我們 package.json 文件中所有的依賴包,而這些依賴包的子依賴包都安裝在了自己的 node_modules 中 ,形成類似于下面的依賴樹:
這樣的目錄有較為明顯的好處:
1)層級結(jié)構(gòu)非常明顯,可以清楚的在第一層的 node_modules 中看到我們安裝的所有包的子目錄;
2)在已知自己所需包的名字以及版本號時,可以復(fù)制粘貼相應(yīng)的文件到 node_modules 中,然后手動更改 package.json 中的配置;
3)如果想要刪除某個包,只需要簡單的刪除 package.json 文件中相應(yīng)的某一行,然后刪除 node_modules 中該包的目錄;
但是這樣的層級結(jié)構(gòu)也有較為明顯的缺陷,當我的 A,B,C 三個包中有相同的依賴 D 時,執(zhí)行 npm install 后,D 會被重復(fù)下載三次,而隨著我們的項目越來越復(fù)雜,node_modules 中的依賴樹也會越來越復(fù)雜,像 D 這樣的包也會越來越多,造成了大量的冗余;在 windows 系統(tǒng)中,甚至會因為目錄的層級太深導(dǎo)致文件的路徑過長,觸發(fā)文件路徑不能超過 280 個字符的錯誤;
為了解決以上問題,npm 3 的 node_modules 目錄改成了更為扁平狀的層級結(jié)構(gòu),盡量把依賴以及依賴的依賴平鋪在 node_modules 文件夾下共享使用。
npm ?3 對于同一依賴的不同版本會怎么處理呢?

但是 npm 3 會帶來一個新的問題:由于在執(zhí)行 npm install 的時候,按照 package.json 里依賴的順序依次解析,上圖如果 C 的順序在 A,B 的前邊,node_modules 樹則會改變,會出現(xiàn)下邊的情況:

由此可見,npm 3 并未完全解決冗余的問題,甚至還會帶來新的問題。
為什么會出現(xiàn) package-lock.json 呢?
為什么會有 package-lock.json 文件呢?這個我們就要先從 package.json 文件說起了。
package.json 的不足之處
npm install 執(zhí)行后,會生成一個 node_modules 樹,在理想情況下, 希望對于同一個 package.json 總是生成完全相同 node_modules 樹。在某些情況下,確實如此。但在多數(shù)情況下,npm 無法做到這一點。有以下兩個原因:
1)某些依賴項自上次安裝以來,可能已發(fā)布了新版本 。比如:A 包在團隊中第一個人安裝的時候是 1.0.5 版本,package.json 中的配置項為 A: '^1.0.5';團隊中第二個人把代碼拉下來的時候,A 包的版本已經(jīng)升級成了 1.0.8,根據(jù) package.json 中的 semver-range version 規(guī)范,此時第二個人 npm install 后 A 的版本為 1.0.8;可能會造成因為依賴版本不同而導(dǎo)致的 bug;
2)針對 1)中的問題,可能有的小伙伴會是把 A 的版本號固定為 A: '1.0.5' 不就可以了嗎?但是這樣的做法其實并沒有解決問題, 比如 A 的某個依賴在第一個人下載的時候是 2.1.3 版本,但是第二個人下載的時候已經(jīng)升級到了 2.2.5 版本,此時生成的 node_modules 樹依舊不完全相同 ,固定版本只是固定來自身的版本,依賴的版本無法固定。
針對 package.json 不足的解決方法
為了解決上述問題以及 npm 3 的問題,在 npm 5.0 版本后,npm install 后都會自動生成一個 package-lock.json 文件 ,當包中有 package-lock.json 文件時,npm install 執(zhí)行時,如果 package.json 和 package-lock.json 中的版本兼容,會根據(jù) package-lock.json 中的版本下載;如果不兼容,將會根據(jù) package.json 的版本,更新 package-lock.json 中的版本,已保證 package-lock.json 中的版本兼容 package.json。
package-lock.json 文件的結(jié)構(gòu)
package-lock.json 文件中的 name、version 與 package.json 中的 name、version 一樣,描述了當前包的名字和版本,dependencies 是一個對象,該對象和 node_modules 中的包結(jié)構(gòu)一一對應(yīng),對象的 key 為包的名稱,值為包的一些描述信息, 根據(jù) package-lock-json官方文檔 (https://docs.npmjs.com/configuring-npm/package-lock-json.html#requires),主要的結(jié)構(gòu)如下:
version:包版本,即這個包當前安裝在node_modules中的版本resolved:包具體的安裝來源integrity:包hash值,驗證已安裝的軟件包是否被改動過、是否已失效requires:對應(yīng)子依賴的依賴,與子依賴的package.json中dependencies的依賴項相同dependencies:結(jié)構(gòu)和外層的dependencies結(jié)構(gòu)相同,存儲安裝在子依賴node_modules中的依賴包
需要注意的是,并不是所有的子依賴都有 dependencies 屬性,只有子依賴的依賴和當前已安裝在根目錄的 node_modules 中的依賴沖突之后,才會有這個屬性。
package-lock.json 文件的作用
在團隊開發(fā)中,確保每個團隊成員安裝的依賴版本是一致的,確定一棵唯一的 node_modules 樹; node_modules 目錄本身是不會被提交到代碼庫的,但是 package-lock.json 可以提交到代碼庫,如果開發(fā)人員想要回溯到某一天的目錄狀態(tài),只需要把 package.json 和 package-lock.json 這兩個文件回退到那一天即可。 由于 package-lock.json 和 node_modules 中的依賴嵌套完全一致,可以更加清楚的了解樹的結(jié)構(gòu)及其變化。 在安裝時,npm 會比較 node_modules 已有的包,和 package-lock.json 進行比較,如果重復(fù)的話,就跳過安裝 ,從而優(yōu)化了安裝的過程。
依賴的區(qū)別與使用場景
npm 目前支持以下幾類依賴包管理包括
dependencies devDependencies optionalDependencies 可選擇的依賴包 peerDependencies 同等依賴 bundledDependencies 捆綁依賴包
下面我們來看一下這幾種依賴的區(qū)別以及各自的應(yīng)用場景:
dependencies
dependencies 是無論在開發(fā)環(huán)境還是在生產(chǎn)環(huán)境都必須使用的依賴,是我們最常用的依賴包管理對象,例如 React,Loadsh,Axios 等,通過 npm install XXX 下載的包都會默認安裝在 dependencies 對象中,也可以使用 ?npm install XXX --save 下載 ?dependencies 中的包;

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

dependencies 和 devDependencies 最大的區(qū)別是在打包運行時,執(zhí)行 npm install 時默認會把所有依賴全部安裝,但是如果使用 npm install --production 時就只會安裝 dependencies 中的依賴,如果是 node 服務(wù)項目,就可以采用這樣的方式用于服務(wù)運行時安裝和打包,減少包大小。
optionalDependencies
optionalDependencies 指的是可以選擇的依賴,當你希望某些依賴即使下載失敗或者沒有找到時,項目依然可以正常運行或者 npm 繼續(xù)運行的時,就可以把這些依賴放在 optionalDependencies 對象中,但是 optionalDependencies 會覆蓋 dependencies 中的同名依賴包,所以不要把一個包同時寫進兩個對象中。
optionalDependencies 就像是我們的代碼的一種保護機制一樣,如果包存在的話就走存在的邏輯,不存在的就走不存在的邏輯。
try?{?
??var?axios?=?require('axios')?
??var?fooVersion?=?require('axios/package.json').version?
}?catch?(er)?{?
??foo?=?null?
}?
//?..?then?later?in?your?program?..?
if?(foo)?{?
??foo.doFooThings()?
}?
peerDependencies
peerDependencies 用于指定你當前的插件兼容的宿主必須要安裝的包的版本,這個是什么意思呢?舉個例子?:我們常用的 react 組件庫 [email protected]?的 package.json (https://github.com/ant-design/ant-design/blob/master/package.json#L37) 中的配置如下:
"peerDependencies":?{?
??"react":?">=16.9.0",?
??"react-dom":?">=16.9.0"?
?},?
假設(shè)我們創(chuàng)建了一個名為 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 文件中手動添加依賴;
bundledDependencies
這個依賴項也可以記為 bundleDependencies,與其他幾種依賴項不同,他不是一個鍵值對的對象,而是一個數(shù)組,數(shù)組里是包名的字符串,例如:
{?
??"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":?"*"?
??},?
如果我們使用常規(guī)的 npm publish 來發(fā)布的話,這個屬性是不會生效的,所以日常情況中使用的較少。
總結(jié)
本文介紹的是 npm 2,npm 3,package-lock.json 以及幾種依賴的區(qū)別和使用場景,希望能夠讓大家對 npm 的了解更加多一點,有什么不清楚的地方或者不足之處歡迎大家在評論區(qū)留言。
參考文獻
package.json官方文檔 (https://docs.npmjs.com/files/package.json#peerdependencies)
package-lock-json官方文檔 (https://docs.npmjs.com/configuring-npm/package-lock-json.html#requires)
npm文檔總結(jié) (https://juejin.im/post/6844903582337237006#heading-0)
npm-pack (https://www.npmjs.cn/cli/pack/)
??看完三件事
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點贊,讓更多的人也能看到介紹內(nèi)容(收藏不點贊,都是耍流氓-_-) 關(guān)注公眾號“前端勸退師”,不定期分享原創(chuàng)知識。 也看看其他文章
勸退師個人微信:huab119
也可以來我的GitHub博客里拿所有文章的源文件:
前端勸退指南:https://github.com/roger-hiro/BlogFN一起玩耍呀
