老項目升級NPM依賴,有哪些注意事項?
想想項目創建之后,多久沒給 npm 依賴升級了?
如何得知當前項目 npm 依賴的“健康度”?
給老項目升級 npm 依賴,有哪些注意事項?
核心訴求
- 提高可維護性。不容易和后引入的依賴產生沖突。引入新特性,功能表現和文檔描述接近,后續開發也能得心應手。
- 提高可移植性。方便老項目向高版本 npm 或 pnpm 遷移。
- 提高可靠性。只要依賴還在穩定迭代,升級必定能引入一系列 bugfix(卻也可能引入新 bug)。
- 提高安全性。官方社區會及時通告 npm 依賴的安全漏洞,將版本保持在安全范圍,能排除許多隱患。
流程方法
- 使用專業的評估工具。手動升級 @latest 等于把依賴當成黑盒來操作。
- 按優先級處理。集中精力升級核心依賴,以及含有安全隱患的庫,否則時間投入很容易超出預期。
- 閱讀 changelog,評估升級影響。
- 回歸測試,十分重要。
除了回歸測試以外,主導治理的人不僅要熟悉項目內容,也要對計劃升級的 npm 包有充分了解。如果沒有合適的人選,建議繼續在代碼堆里堅持一會兒,畢竟升級有風險,后果得自負。
檢索工具
以下內容以 npm 為例,pnpm 和 yarn 有可替代的命令。
過時依賴 npm outdated
npm outdated 命令會從 npm 源檢查已安裝的軟件包是否已過時。
隨便拿幾個包舉例:
Package Current Wanted Latest Location
axios 0.18.1 0.18.1 0.27.2 project-dir
log4js 2.11.0 2.11.0 6.5.2 project-dir
lru-cache 4.1.5 4.1.5 7.10.2 project-dir
socket.io 2.4.1 2.5.0 4.5.1 project-dir
vue 2.6.14 2.6.14 3.2.37 project-dir
vue-lazyload 1.3.3 1.3.4 3.0.0-rc.2 project-dir
vue-loader 14.2.4 14.2.4 17.0.0 project-dir
vue-router 3.5.3 3.5.4 4.0.16 project-dir
vuex 3.6.2 3.6.2 4.0.2 project-dir
webpack 3.12.0 3.12.0 5.73.0 project-dir
默認只會檢查項目 package.json 中直接引用的依賴, --all 選項可以用來匹配全部的依賴。但沒有必要,真要徹底升級,更推薦嘗試重建 lock 文件。
對于 outdated 的包,使用 npm update 或其他包管理工具對應的 update 命令即可安裝 SemVer 標準執行升級。如果想跨越 Major 版本,則需要手動指定升級版本。
風險依賴 npm audit
npm audit 命令同樣是向 npm 源發起請求,它將 package-lock.json 作為參數,返回存在已知漏洞的依賴列表。換句話說,audit 不需要安裝 node_modules 就可以執行,其結果完全取決于當前的 package-lock.json。
返回節選如下:
# Run npm install swiper.2.5 to resolve 1 vulnerability
SEMVER WARNING: Recommended action is a potentially breaking change
┌───────────────┬──────────────────────────────────────────────────────────────┐
│ Critical │ Prototype Pollution in swiper │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package │ swiper │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ swiper │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path │ swiper │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info │ https://github.com/advisories/GHSA-p3hc-fv2j-rp68 │
└───────────────┴──────────────────────────────────────────────────────────────┘
found 125 vulnerabilities (8 low, 66 moderate, 41 high, 10 critical) in 2502 scanned packages
run `npm audit fix` to fix 15 of them.
96 vulnerabilities require semver-major dependency updates.
14 vulnerabilities require manual review. See the full report for details.
如果你發現執行結果為 404,說明當前源不支持 audit 接口,可更換到支持 audit 的官方源重新執行。
npm http fetch POST 404 https://registry.npmmirror.com/-/npm/v1/security/audits 306ms
npm ERR! code ENOAUDIT
npm ERR! audit Your configured registry (https://registry.npmmirror.com/) does not support audit requests.
npm ERR! audit The server said: <h1>404 Not Found</h1>
結果中雖然提到了 npm audit fix 命令,卻不總是可靠的,它能修復的依賴有限,遠不如通過升級 root 依賴修復間接依賴帶來的數量明顯。
隱式依賴 npx depcheck
npm cli 工具 depcheck 能輔助我們找到項目中 Unused dependencies(無用依賴)和 Phantom dependencies(幻影依賴),分別表示寫入 package.json 但沒被項目使用、被項目引用了但沒有寫入 package.json。
depcheck 更像是一個縮小排查范圍的過濾器,不能輕信其打印結果。例如,depcheck 默認無法識別特殊掛載的 plugin。
Unused dependencies
* clipboard
* cross-env
* firebase
* proxy
* route-cache
* socket.io
Unused devDependencies
* add-asset-html-webpack-plugin
* commitizen
* eslint
* husky
* jasmine
* rimraf
* stylelint
Missing dependencies
* node-notifier: ./build/utils.js
要刪除一個無用依賴,必須熟悉該 npm 包的使用性質,再結合 grep 工具反復確認。
僵尸依賴 npm install
最后,還要提防一種 Zombie dependencies(僵尸依賴)。不同于前面介紹的隱式依賴,它的危害很大。
首先它切實被項目使用,但已經被維護者 deprecated 或 archieved。意味著版本不再更新,包名不會出現在 outdated 列表;很可能沒人報告漏洞,也不會出現在 audit 列表。但潛在的 bug 無人修復,它將一直躲藏在項目里,伺機而動。
筆者沒發現合適的工具去尋找僵尸依賴,只好多留意 npm install 的 deprecated 日志。
治理建議
如何閱讀 CHANGELOG
changelog 一般位于代碼倉庫的 CHANGELOG.md 或 History.md,隨意一些的也可能放在在 Github 的 releases 頁,正式一些的會放在官方網站的 Migrations 類目。
如果發現一個 npm 包沒有 changelog,或 changelog 寫得太差,建議換成其他更靠譜的替代品 ,就只能靠閱讀 commits 了。
關鍵詞(歡迎補充):
- BREAKING CHANGE
- !
- Node.js
開發者普遍會用上面的方式標注不兼容的變更。
lock 文件版本管理
該建議是對商業軟件的研發流程而言。活躍的開源場景并不需要 lock 文件,為了開發者迭代和測試的過程能趁早發現兼容性問題。
package-lock.json 的設計文稿就直言推薦把 lock 文件加入代碼倉庫:
- 保證團隊成員和 CI 能使用完全相同的依賴關系。
- 作為 node_modules 的輕量化備份。
- 讓依賴樹的變化更具可見性。
- 加速安裝過程。
但是,npm 依賴管理的策略因團隊和項目而異,是否提交 lock 文件到 git 倉庫可以按需取舍,版本管理的形式還有很多。
例如研發流程完善,每次發布的 lock 文件都會留在制品庫或鏡像中,能夠隨時被還原。可如果缺少相關舉措,就要想辦法將生產環境的 lock 文件備份,為問題復現、故障恢復提供依據。
更新 hoisting
常年累月的更新之下,許多 package-lock.json 的外層依賴的版本會落后于子節點,因為目前 npm 為了保持最小更新幅度,不會對 lock 樹做旋轉和變形。即使更新的項目的直接依賴到 latest,它的間接依賴可能還是舊的,以致現存的依賴提升結果和默認 hoisting 算法的偏差越來越大。
一些老項目脫離 package-lock.json 文件之后,甚至無法正常安裝構建。此時依賴已經處于非常不健康的狀態,開發者需要擔心新引入的依賴是否會破壞平衡,無法遷移 npm 包管理工具,也不能升級 Node.js 版本。不過亡羊補牢并不復雜,總好過修復一個沒有 package-lock.json 的項目。
想生成一份可靠的 package-lock.json,最簡單的辦法就是除舊迎新:
rm -rf package-lock.json node_modules
npm i
更好的辦法是換到不使用 hoisting 的依賴管理工具。
情況講清楚了,什么時候重建可以看自身需求。但是,將 lock 文件加入 .gitignore 的同學就要注意了,如果別人出現了你本地無法復現的問題,記得先刪掉 package-lock.json。
整理 dependencies 和 devDependencies
package.json 中 dependencies 和 devDependencies 的區別就不必介紹了,但大家在項目中是否會做嚴格區分呢?
一來 devDependencies 是為 npm 包優化依賴關系設計的,作為應用的項目通常不會打包發布到 npm 上;二來不作區分也沒有直接帶來不良后果。因此經常會有小伙伴將開發環境依賴的工具直接安裝到 dependencies 中。
不過,即使對項目而言,devDependencies 也有積極意義:
- 能從語義上劃分依賴的用途。
- 使用 npm install --production 可以忽略 devDependencies,提高安裝效率,顯著減少 node_modules 的體積。
第二點還需要做個補充說明,由于靜態項目的構建環境往往需要安裝大部分 devDependencies 中的依賴,一般只有放在服務端運行的 Node.js 項目才需要考慮這么做。但隨著 TypeScript 的普及或是 SSR 的引入,這些服務端項目在運行前也需要執行構建。那還有什么用?別忘了,還有一個 npm prune --production 能用作后置的項目體積優化。
當然了,語義劃分帶來幫助也足夠大了,例如根據依賴關系來優化 npm 治理的優先級和策略。
順便再提一句,dependencies 和 devDependencies 不是用來區分重要程度,請不要把運行可有可無的依賴放在 devDependencies,應該放在optionalDependencies (https://docs.npmjs.com/cli/v6/configuring-npm/package-json#optionaldependencies) 中。
結語
以上介紹的經驗多為概述,主要結合 npm 依賴管理工具的特點,沒能介紹 yarn 和 pnpm 等工具獨有的 API 和問題,如果讀者想了解更多內容,請查閱相關文檔。此外,同時使用多種依賴管理工具的項目頗為復雜,比較少見,本文未作分析,也不建議讀者朋友們嘗試。而在軟件工程領域,依賴治理還有很多要點需要我們去進一步實踐,不過內容更側重于 refactor。
回到標語所提項目依賴的“健康度”,實為筆者胡謅,用來形容依賴關系的混亂程度。不做這些依賴治理,也沒有太大關系,因為軟件的生命周期往往堅持不到依賴關系崩壞的那天。但混亂的依賴管理,卻能輕易促成代碼的腐化。