你可能不知道的 npm 依賴管理那些事

點(diǎn)擊上方藍(lán)字關(guān)注我們
npm 是 Node.js 默認(rèn)的、以 JavaScript 編寫的包管理工具,如今,它已經(jīng)成為世界上最大的包管理工具,是每個(gè)前端開發(fā)者必備的工具。不知你是否遇到過下面問題:
哎?我本地明明是好的,線上的依賴怎么就報(bào)錯(cuò)不行了呢?一言不合就刪除整個(gè)
node_modules目錄然后重新npm install
今天我們聊聊npm模塊相關(guān)的東西。
semver
npm 依賴管理的一個(gè)重要特性是采用了語義化版本 (semver)?規(guī)范,作為依賴版本管理方案。
semver規(guī)定的模塊版本號(hào)格式為:MAJOR.MINOR.PATCH,即主版本號(hào).次版本號(hào).修訂號(hào)。版本號(hào)遞增規(guī)則如下:
主版本號(hào):當(dāng)你做了不兼容的 API 修改,例如新增了breaking change。
次版本號(hào):當(dāng)你做了向下兼容的功能性新增,例如新增feature。
修訂號(hào):當(dāng)你做了向下兼容的問題,例如修復(fù)bug。
對(duì)于npm包的引用者來說,經(jīng)常會(huì)在package.json文件里面看到使用semver約定的semver range來指定所需的依賴包版本號(hào)和版本范圍。常用的規(guī)則如下表:

此外,任意兩條規(guī)則,用空格連接起來,表示“與”邏輯,即兩條規(guī)則的交集: 如?>=2.3.1 <=2.8.0?可以解讀為:?>=2.3.1?且?<=2.8.0。
任意兩條規(guī)則,通過?||?連接起來,表示“或”邏輯,即兩條規(guī)則的并集: 如?^2 >=2.3.1 || ^3 >3.2。
在修訂版本號(hào)的后面可以加上其他信息,用-連接,比如:
X.Y.Z-Alpha: 內(nèi)測版
X.Y.Z-Beta: 公測版
X.Y.Z-Stable: 穩(wěn)定版
從 npm install 說起
npm install 命令用來安裝模塊到 node_modules 目錄。npm install 的具體原理是什么呢?
執(zhí)行工程自身 preinstall
確定首層依賴模塊
首層依賴是 package.json 中 dependencies 和 devDependencies 字段直接指定的模塊。每一個(gè)首層依賴模塊都是模塊依賴樹根節(jié)點(diǎn)下面的一顆子樹。
獲取模塊
獲取模塊是一個(gè)遞歸的過程,分為以下幾步:
獲取模塊信息。在下載一個(gè)模塊之前,首先要確定其版本,這是因?yàn)?package.json?中的模塊版本往往是 semantic version。此時(shí)根據(jù)package.json和版本描述文件(npm-shrinkwrap.json?或?package-lock.json不同npm版本的策略不同,后續(xù)我們會(huì)詳細(xì)介紹)。如?package.json?中某個(gè)包的版本是?
^1.1.0,npm 就會(huì)去倉庫中獲取符合1.x.x形式的最新版本。獲取模塊實(shí)體。上一步會(huì)獲取到模塊的壓縮包地址(resolved 字段),npm 會(huì)用此地址檢查本地緩存,緩存中有就直接拿,如果沒有則從倉庫下載。
查找該模塊依賴,如果有依賴則回到第1步,如果沒有則停止。
模塊扁平化 (npm3 后支持)
上一步獲取到的是一顆完整的依賴樹,下面會(huì)根據(jù)依賴樹安裝模塊。模塊安裝機(jī)制有兩種:嵌套式安裝機(jī)制?和?扁平式安裝機(jī)制。
例如某工程下直接依賴了A和B兩個(gè)包,且他們同時(shí)依賴了C包。
嵌套式

npm3之前使用的是嵌套式安裝機(jī)制,嚴(yán)格按照依賴樹的結(jié)構(gòu)進(jìn)行安裝,這可能會(huì)造成相同模塊大量冗余的問題。
扁平式

npm3之后使用的扁平式安裝機(jī)制,但是需要考慮一個(gè)問題:
工程同時(shí)依賴一個(gè)模塊不同版本該如何解決?
npm3 引入了 dedupe 過程來解決這個(gè)問題。它會(huì)遍歷所有節(jié)點(diǎn),逐個(gè)將模塊放在根節(jié)點(diǎn)下面,也就是 node-modules 的第一層。當(dāng)發(fā)現(xiàn)有重復(fù)模塊時(shí),則將其丟棄。
重復(fù)模塊:semver兼容的相同模塊。例如 lodash ^1.2.0和lodash ^1.4.0。如果工程的兩個(gè)模塊版本范圍存在交集,就可以得到一個(gè)?兼容版本,不必版本號(hào)完全一致,這可以使得更多冗余模塊在dedupe過程中被去掉。
上例中如果A包依賴[email protected],B包依賴[email protected],此時(shí)兩個(gè)版本并不兼容,則后面的版本仍會(huì)保留在依賴書中。如下圖所示:

實(shí)際上,npm3仍然可能出現(xiàn)模塊冗余的情況,如下圖,因?yàn)橐患?jí)目錄下已經(jīng)有[email protected],所以所有的[email protected]只能作為二級(jí)依賴模塊被安裝:

npm提供了?npm dedupe?指令來優(yōu)化依賴樹結(jié)構(gòu)。這個(gè)命令會(huì)去搜索本地的node_modules中的包,并且通過移動(dòng)相同的依賴包到外層目錄去盡量簡化這種依賴樹的結(jié)構(gòu),讓公用包更加有效被引用。
安裝模塊
將會(huì)更新工程中的?node_modules,并執(zhí)行模塊中的生命周期函數(shù)(按照?preinstall、install、postinstall?的順序)
執(zhí)行工程自身生命周期
當(dāng)前 npm 工程如果定義了鉤子此時(shí)會(huì)被執(zhí)行(按照?install、postinstall、prepublish、prepare?的順序)。最后生成或者更新版本描述文件。
你是否遇到過本地開發(fā)時(shí)一切正常,發(fā)布線上代碼時(shí)因?yàn)榘惭b依賴的錯(cuò)誤導(dǎo)致服務(wù)不可用?如果是的話,你要一份版本描述文件。
簡單的寫死當(dāng)前工程依賴模塊的版本并不能真正鎖定依賴版本,因?yàn)槟銦o法控制間接依賴,如果間接依賴更新了有問題的模塊,你的系統(tǒng)還是可能會(huì)有宕機(jī)的風(fēng)險(xiǎn)。
lock 文件是當(dāng)前依賴關(guān)系樹的快照,允許不同機(jī)器間的重復(fù)構(gòu)建。其實(shí) npm5 之前已經(jīng)提供了lock文件——?npm-shrinkwrap.json。但是在 npm5 發(fā)布的時(shí)候創(chuàng)建了新的lock文件——?package-lock.json,其主要目的是希望能更好的傳達(dá)一個(gè)消息,npm真正支持了locking機(jī)制。不過二者還是有一些區(qū)別點(diǎn):
?發(fā)布npm包時(shí),package-lock.json?不會(huì)被發(fā)布, 即使你將其顯式添加到軟件包的?files?屬性中,它也不會(huì)是已發(fā)布軟件包的一部分。npm-shrinkwrap.json?可以被發(fā)布。
npm-shrinkwrap.json向后兼容npm2、3、4版本,package-lock.json 只有 npm5 以上支持。
可以通過
npm shrinkwrap命令將package-lock.json轉(zhuǎn)換成npm-shrinkwrap.json, 因?yàn)槲募母袷绞峭耆粯拥摹?/span>
查閱資料得知,自npm 5.0版本發(fā)布以來,package-lock.json的規(guī)則發(fā)生了三次變化。
npm 5.0.x版本,不管 package.json 怎么變,
npm install都會(huì)根據(jù)lock文件下載。npm/npm#16866 控訴了這個(gè)問題,我明明手動(dòng)改了 package.json ,為啥不給我升包!然后就導(dǎo)致5.1.0的問題(是個(gè)bug)npm 5.1.0 - 5.4.1版本,
npm insall會(huì)無視lock文件,去下載semver兼容的最新的包。導(dǎo)致lock文件并不能完全鎖住依賴樹。詳情見npm/npm#17979npm 5.4.2版本之后,如果手動(dòng)改了package.json,且package.json和lock文件不同,那么執(zhí)行
npm install時(shí) npm 會(huì)根據(jù) package 中的版本號(hào)和語義含義去下載最新的包,并更新至 lock。如果兩者是同一狀態(tài),那么執(zhí)行?
npm install都會(huì)根據(jù) lock 下載,不會(huì)理會(huì) package 實(shí)際包的版本是否更新。
好的依賴管理方案
使用 npm: >=5.4.2 版本, 保持?package-lock.json?文件默認(rèn)開啟配置
初始化:第一作者初始化項(xiàng)目時(shí)使用?
npm install?安裝依賴包, 默認(rèn)保存?^X.Y.Z?依賴 range 到 package.json 中; 提交?package.json,?package-lock.json,?不要提交?node_modules?目錄初始化:項(xiàng)目成員首次 checkout/clone 項(xiàng)目代碼后,執(zhí)行一次?
npm install?安裝依賴包升級(jí)依賴包:
升級(jí)小版本: 本地執(zhí)行?
npm update?升級(jí)到新的小版本升級(jí)大版本: 本地執(zhí)行?
升級(jí)到新的大版本
也可手動(dòng)修改 package.json 中版本號(hào)為要升級(jí)的版本(大于現(xiàn)有版本號(hào))并指定所需的 semver, 然后執(zhí)行?
npm install本地驗(yàn)證升級(jí)后新版本無問題后,提交新的?package.json,?package-lock.json?文件
降級(jí)依賴包:
刪除依賴包:
Plan A:?
Plan B: 把要卸載的包從 package.json 中 dependencies 字段刪除, 然后執(zhí)行?
npm install?并提交?package.json?和?package-lock.json任何時(shí)候有人提交了?package.json,?package-lock.json?更新后,團(tuán)隊(duì)其他成員應(yīng)在?
svn update/git pull?拉取更新后執(zhí)行?npm install?腳本安裝更新后的依賴包不要手動(dòng)修改 package-lock.json
當(dāng) package-lock.json 出現(xiàn)沖突時(shí),這種是非常棘手的情況,最好不要手動(dòng)解決沖突,如果有一處沖突解決不正確可能會(huì)導(dǎo)致線上事故。
建議的做法:將本地的 package-lock.json文件刪除,引入遠(yuǎn)程的 package-lock.json 文件,再執(zhí)行npm install命令更新package-lock.json文件。(這種做法能保證未修改的依賴不變,會(huì)存在一個(gè)風(fēng)險(xiǎn):在執(zhí)行
npm install的時(shí)候,可能有些間接依賴包升級(jí),根據(jù)semver兼容原則導(dǎo)致本次安裝的和開發(fā)時(shí)的package-lock.json文件不同。這種情況就需要驗(yàn)證依賴包升級(jí)是否有影響)部署安裝依賴時(shí),執(zhí)行
npm install命令。不要執(zhí)行npm install
命令,因?yàn)檫@會(huì)導(dǎo)致 package-lock.json 文件同時(shí)被更新。
npm install @ 正確:?
npm install @ 驗(yàn)證無問題后,提交?package.json 和 package-lock.json 文件
npm uninstall ????并提交?package.json?和?package-lock.json
問題來了
git diff files(git diff-tree -r --name-only --no-commit-id HEAD@{1} HEAD) 是否包含了 package.json 文件,如果包含了該文件,則執(zhí)行npm install命令。我們暫且給這個(gè)插件取名為 hawkeye 。當(dāng)然,這個(gè)插件能干的事情不僅于此。
不知作為讀者的你聽到上述場景描述后,是否有種似曾相識(shí)的感覺?沒錯(cuò),lint-staged。
lint-staged,從git staged files變化中匹配你想要的文件,再執(zhí)行你配置的commands。
Hawkeye,從git diff files變化中匹配你想要的文件,再執(zhí)行你配置的commands。
需要注意的是,他們都依賴于husky改造git hooks的能力。
實(shí)現(xiàn)方案

例子
假設(shè)有一個(gè)已經(jīng)安裝了 hawkeye 和 husky 的項(xiàng)目, package.json 如下:
{"name": "My project","version": "0.1.0","scripts": {},"husky": {"hooks": {"post-merge": "hawkeye"}},"hawkeye": {"package.json": ["npm install"]}}
semver 語義化版本? https://semver.org/lang/zh-CN/?spm=ata.13261165.0.0.552e2688ZKTpgz
semver(1) -- The semantic versioner for npm
https://github.com/npm/node-semver?spm=ata.13261165.0.0.552e2688ZKTpgz
2018 年了,你還是只會(huì) npm install 嗎?
https://juejin.im/post/5ab3f77df265da2392364341?spm=ata.13261165.0.0.552e2688ZKTpgz
npm install algorithm
https://docs.npmjs.com/cli/install?spm=ata.13261165.0.0.552e2688ZKTpgz#algorithm
npm dedupe
https://docs.npmjs.com/cli/dedupe.html?spm=ata.13261165.0.0.552e2688ZKTpg
npm install的實(shí)現(xiàn)原理
https://www.zhihu.com/question/66629910?spm=ata.13261165.0.0.552e2688ZKTpgz
[譯] 理解 NPM 5 中的 lock 文件
https://juejin.im/post/5943849aac502e006b84ce07?spm=ata.13261165.0.0.552e2688ZKTpgz
package-lock.json file not updated after package.json file is changed
https://github.com/npm/npm/issues/16866?spm=ata.13261165.0.0.552e2688ZKTpgz
why is package-lock being ignored?
https://github.com/npm/npm/issues/17979?spm=ata.13261165.0.0.552e2688ZKTpgz
lint-staged
https://github.com/okonet/lint-staged?spm=ata.13261165.0.0.552e2688ZKTpgz
hawkeye
https://github.com/stormqx/hawkeye?spm=ata.13261165.0.0.552e2688ZKTpgz
推薦閱讀
