
點擊上方藍字關注我們
npm 是 Node.js 默認的、以 JavaScript 編寫的包管理工具,如今,它已經成為世界上最大的包管理工具,是每個前端開發者必備的工具。不知你是否遇到過下面問題:
哎?我本地明明是好的,線上的依賴怎么就報錯不行了呢?一言不合就刪除整個
node_modules
目錄然后重新npm install
今天我們聊聊npm模塊相關的東西。
semver
npm 依賴管理的一個重要特性是采用了語義化版本 (semver)?規范,作為依賴版本管理方案。
semver規定的模塊版本號格式為:MAJOR.MINOR.PATCH
,即主版本號.次版本號.修訂號
。版本號遞增規則如下:
主版本號:當你做了不兼容的 API 修改,例如新增了breaking change。
次版本號:當你做了向下兼容的功能性新增,例如新增feature。
修訂號:當你做了向下兼容的問題,例如修復bug。
對于npm包的引用者來說,經常會在package.json
文件里面看到使用semver約定的semver range來指定所需的依賴包版本號和版本范圍。常用的規則如下表:
此外,任意兩條規則,用空格連接起來,表示“與”邏輯,即兩條規則的交集: 如?>=2.3.1 <=2.8.0
?可以解讀為:?>=2.3.1
?且?<=2.8.0
。
任意兩條規則,通過?||
?連接起來,表示“或”邏輯,即兩條規則的并集: 如?^2 >=2.3.1 || ^3 >3.2
。
在修訂版本號的后面可以加上其他信息,用-
連接,比如:
X.Y.Z-Alpha: 內測版
X.Y.Z-Beta: 公測版
X.Y.Z-Stable: 穩定版
從 npm install 說起
npm install 命令用來安裝模塊到 node_modules 目錄。npm install 的具體原理是什么呢?
執行工程自身 preinstall
確定首層依賴模塊
首層依賴是 package.json 中 dependencies 和 devDependencies 字段直接指定的模塊。每一個首層依賴模塊都是模塊依賴樹根節點下面的一顆子樹。
獲取模塊
獲取模塊是一個遞歸的過程,分為以下幾步:
獲取模塊信息。在下載一個模塊之前,首先要確定其版本,這是因為?package.json?中的模塊版本往往是 semantic version。此時根據package.json和版本描述文件(npm-shrinkwrap.json?或?package-lock.json不同npm版本的策略不同,后續我們會詳細介紹)。如?package.json?中某個包的版本是?
^1.1.0
,npm 就會去倉庫中獲取符合1.x.x
形式的最新版本。獲取模塊實體。上一步會獲取到模塊的壓縮包地址(resolved 字段),npm 會用此地址檢查本地緩存,緩存中有就直接拿,如果沒有則從倉庫下載。
查找該模塊依賴,如果有依賴則回到第1步,如果沒有則停止。
模塊扁平化 (npm3 后支持)
上一步獲取到的是一顆完整的依賴樹,下面會根據依賴樹安裝模塊。模塊安裝機制有兩種:嵌套式安裝機制?和?扁平式安裝機制。
例如某工程下直接依賴了A和B兩個包,且他們同時依賴了C包。
嵌套式
npm3之前使用的是嵌套式安裝機制,嚴格按照依賴樹的結構進行安裝,這可能會造成相同模塊大量冗余的問題。
扁平式
npm3之后使用的扁平式安裝機制,但是需要考慮一個問題:
工程同時依賴一個模塊不同版本該如何解決?
npm3 引入了 dedupe 過程來解決這個問題。它會遍歷所有節點,逐個將模塊放在根節點下面,也就是 node-modules 的第一層。當發現有重復模塊時,則將其丟棄。
重復模塊:semver兼容的相同模塊。例如lodash ^1.2.0
和lodash ^1.4.0
。如果工程的兩個模塊版本范圍存在交集,就可以得到一個?兼容版本,不必版本號完全一致,這可以使得更多冗余模塊在dedupe過程中被去掉。
上例中如果A包依賴C@1.0.0
,B包依賴C@2.0.0
,此時兩個版本并不兼容,則后面的版本仍會保留在依賴書中。如下圖所示:
實際上,npm3仍然可能出現模塊冗余的情況,如下圖,因為一級目錄下已經有C@1.0.0
,所以所有的C@2.0.0
只能作為二級依賴模塊被安裝:
npm提供了?npm dedupe?指令來優化依賴樹結構。這個命令會去搜索本地的node_modules
中的包,并且通過移動相同的依賴包到外層目錄去盡量簡化這種依賴樹的結構,讓公用包更加有效被引用。
安裝模塊
將會更新工程中的?node_modules,并執行模塊中的生命周期函數(按照?preinstall、install、postinstall?的順序)
執行工程自身生命周期
當前 npm 工程如果定義了鉤子此時會被執行(按照?install、postinstall、prepublish、prepare?的順序)。最后生成或者更新版本描述文件。
鎖定npm依賴版本你是否遇到過本地開發時一切正常,發布線上代碼時因為安裝依賴的錯誤導致服務不可用?如果是的話,你要一份版本描述文件。
簡單的寫死當前工程依賴模塊的版本并不能真正鎖定依賴版本,因為你無法控制間接依賴,如果間接依賴更新了有問題的模塊,你的系統還是可能會有宕機的風險。
lock 文件是當前依賴關系樹的快照,允許不同機器間的重復構建。其實 npm5 之前已經提供了lock文件——?npm-shrinkwrap.json。但是在 npm5 發布的時候創建了新的lock文件——?package-lock.json,其主要目的是希望能更好的傳達一個消息,npm真正支持了locking機制。不過二者還是有一些區別點:
?發布npm包時,package-lock.json?不會被發布, 即使你將其顯式添加到軟件包的?files?屬性中,它也不會是已發布軟件包的一部分。npm-shrinkwrap.json?可以被發布。
npm-shrinkwrap.json向后兼容npm2、3、4版本,package-lock.json 只有 npm5 以上支持。
可以通過
npm shrinkwrap
命令將package-lock.json轉換成npm-shrinkwrap.json, 因為文件的格式是完全一樣的。
查閱資料得知,自npm 5.0版本發布以來,package-lock.json
的規則發生了三次變化。
npm 5.0.x版本,不管 package.json 怎么變,
npm install
都會根據lock文件下載。npm/npm#16866 控訴了這個問題,我明明手動改了 package.json ,為啥不給我升包!然后就導致5.1.0的問題(是個bug)npm 5.1.0 - 5.4.1版本,
npm insall
會無視lock文件,去下載semver兼容的最新的包。導致lock文件并不能完全鎖住依賴樹。詳情見npm/npm#17979npm 5.4.2版本之后,如果手動改了package.json,且package.json和lock文件不同,那么執行
npm install
時 npm 會根據 package 中的版本號和語義含義去下載最新的包,并更新至 lock。如果兩者是同一狀態,那么執行?
npm install
都會根據 lock 下載,不會理會 package 實際包的版本是否更新。
好的依賴管理方案
使用 npm: >=5.4.2 版本, 保持?package-lock.json?文件默認開啟配置
初始化:第一作者初始化項目時使用?
npm install
?安裝依賴包, 默認保存?^X.Y.Z
?依賴 range 到 package.json 中; 提交?package.json,?package-lock.json,?不要提交?node_modules?目錄初始化:項目成員首次 checkout/clone 項目代碼后,執行一次?
npm install
?安裝依賴包升級依賴包:
升級小版本: 本地執行?
npm update
?升級到新的小版本升級大版本: 本地執行?
npm install @
升級到新的大版本
也可手動修改 package.json 中版本號為要升級的版本(大于現有版本號)并指定所需的 semver, 然后執行?
npm install
本地驗證升級后新版本無問題后,提交新的?package.json,?package-lock.json?文件
降級依賴包:
正確:?
npm install @
驗證無問題后,提交?package.json 和 package-lock.json 文件
刪除依賴包:
Plan A:?
npm uninstall
????并提交?package.json?和?package-lock.json
Plan B: 把要卸載的包從 package.json 中 dependencies 字段刪除, 然后執行?
npm install
?并提交?package.json?和?package-lock.json
任何時候有人提交了?package.json,?package-lock.json?更新后,團隊其他成員應在?svn update/git pull
?拉取更新后執行?npm install
?腳本安裝更新后的依賴包
不要手動修改 package-lock.json
當 package-lock.json 出現沖突時,這種是非常棘手的情況,最好不要手動解決沖突,如果有一處沖突解決不正確可能會導致線上事故。
建議的做法:將本地的 package-lock.json文件刪除,引入遠程的 package-lock.json 文件,再執行npm install
命令更新package-lock.json文件。
(這種做法能保證未修改的依賴不變,會存在一個風險:在執行npm install
的時候,可能有些間接依賴包升級,根據semver兼容原則導致本次安裝的和開發時的package-lock.json文件不同。這種情況就需要驗證依賴包升級是否有影響)
部署安裝依賴時,執行npm install
命令。不要執行
npm install
命令,因為這會導致 package-lock.json 文件同時被更新。
問題來了
上述最佳實踐提到了當團隊中有成員提交了?package.json,?package-lock.json?更新后,其他成員需要執行?npm install 來保證本地依賴的及時性,那么能否寫一個插件將這個手動的環節自動化呢?答案是可以的,我們只需要在 git post-merge 鉤子中檢查git diff files(git diff-tree -r --name-only --no-commit-id HEAD@{1} HEAD)
是否包含了 package.json 文件,如果包含了該文件,則執行npm install
命令。我們暫且給這個插件取名為 hawkeye 。當然,這個插件能干的事情不僅于此。
不知作為讀者的你聽到上述場景描述后,是否有種似曾相識的感覺?沒錯,lint-staged。
lint-staged,從git staged files變化中匹配你想要的文件,再執行你配置的commands。
Hawkeye,從git diff files變化中匹配你想要的文件,再執行你配置的commands。
需要注意的是,他們都依賴于husky改造git hooks的能力。
實現方案
例子
假設有一個已經安裝了 hawkeye 和 husky 的項目, 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 年了,你還是只會 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的實現原理
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
推薦閱讀
我的公眾號能帶來什么價值?(文末有送書規則,一定要看)每個前端工程師都應該了解的圖片知識(長文建議收藏)為什么現在面試總是面試造火箭?