前言
作為前端開發者,npm這個包管理工具的重要性顯而易見。優點不再表述,但一些缺點是為使用者詬病比較多的:速度慢、版本控制。下面主要討論下npm的版本固化問題,即lock檔案。
npm語義化版本管理
對於npm來說,依賴相關的資訊體現在package.json的dependencies裡,這裡使用了Semver(語義化版本來控制)關於語義化版本的規範可以檢視。
大致準則如下:
- 軟體的版本通常由三位組成,形如:X.Y.Z
- 版本是嚴格遞增的,此處是:16.2.0 -> 16.3.0 -> 16.3.1
- 在釋出重要版本時,可以釋出alpha, rc等先行版本
- alpha和rc等修飾版本的關鍵字後面可以帶上次數和meta資訊
版本格式:
釋出者應該關注的是版本格式的規則
主版本號.次版本號.修訂號
不同版本號遞增規則如下:
- 主版本號(major):當你做了不相容的 API 修改,
- 次版本號(minor):當你做了向下相容的功能性新增,可以理解為Feature版本,
- 修訂號(patch):當你做了向下相容的問題修正,可以理解為Bug fix版本。
package.json裡面的依賴版本要求遵循上述規則的。
這樣才能保證使用者引到期望的版本。
版本控制符
對於使用者來說,版本前面的控制符是需要關注的,這決定引用依賴是否與期望相同。
npm 支援的符號是比較豐富的,下面的版本符號均支援:
{ "dependencies" :
{ "foo" : "1.0.0 - 2.9999.9999",// 大於等於1.0.0 小於 2.9999.9999
"bar" : ">=1.0.2 <2.1.2", // 比較清晰 左閉右開
"baz" : ">1.0.2 <=2.3.4", // 左開右閉
"boo" : "2.0.1", // 規定版本
"qux" : "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0", // 表示式也算清晰
"asd" : "http://asdf.com/asdf.tar.gz"// 指定下載地址代替版本
, "til" : "^1.2.3"// 同一主版本號,不小於1.2.3 即 1.x.y x>=2 y>=3
, "elf" : "~1.2.3" // 同一主版本和次版本號 即1.2.x x>= 2
, "two" : "2.x" // 這個比較形象,x>=0 即2.0.0 以上均可
, "thr" : "3.3.x" // 同上 x>= 0 即3.3.0 以上
, "lat" : "latest" // 最新版本
, "dyl" : "file:../dyl" // 從本地下載
}
}
根據上面的註釋,應該能看清不同符號的意思了。這裡有一篇文章比較生動的闡述了不同符號的範圍,有興趣可以詳細看下
npm install 預設使用^
這樣的目的在於接受指定版本的更新,例如依賴包的優化和小版本更新。
問題
不同環境依賴不一致
從上面可以看到,語義化版本是沒有強制約束的,需要開發者自覺遵守規範定義。
常見情況如下:
測試環境完成之後上線出問題,細究原因在於上線前某個依賴版釋出了不相容或者有bug 版本,恰好在釋出時裝了新版本。
所以才會有下面固化版本即鎖版本需求的出現。
固化版本,保證不同環境或者時間安裝的都是相同依賴。至於是否都應該固化版本,下面再討論。
固化版本方式
固話版本,有下面三種方式:
npm-shrinkwrap.json
該方式是比較早的鎖定版本的方式,
與package-lock.json功能類似,區別在於npm包釋出的時候可以釋出上去。
推薦的使用情況是,通過倉庫上的釋出過程來部署的應用,即非庫或者工具類。
例如:emons和命令列工具,想要被全域性安裝或者依賴,此時強烈不建議坐著釋出該檔案,因為將會阻止終端使用者控制傳遞依賴的更新。
另外如果package-lock.json和npm-shrinkwrap.json同時存在於專案根目錄,package-lock.json將會被忽略。
即該方式會將鎖版本依賴通過npm釋出,所以類庫或者元件需要慎重。
使用方式:
// 生成依賴 預設不包括dev dependencies
npm shrinkwrap
// 將dev-dependencies計算在內
npm shrinkwrap--dev
package-lock.json
相對於npm-shrinkwrap ,其不會被髮布到npm,適用於應用程式,即我們非工具類的專案。
npm5 以後 依賴都會預設增加該檔案,不過迭代了這麼多版本,不同版本npm對package-lock.json的實現是不同的。是在一直迭代和發展的
1、npm 5.0.x 版本,
不管package.json怎麼變,npm i 時都會根據lock檔案下載。
2、5.1.0版本後
npm install 會無視lock檔案 去下載最新的npm包
3、5.4.2版本
如果改了package.json,且package.json和lock檔案不同,那麼執行npm i
時npm會根據package中的版本號以及語義含義去下載最新的包,並更新至lock。
如果兩者是同一狀態,那麼執行npm i
都會根據lock下載,不會理會package實際包的版本是否有新。
該段內容參考自知乎使用者,詳情請轉https://www.zhihu.com/question/264560841
這樣帶來一個問題,不同環境不同npm版本,對於同一專案,依賴還是可能不同的。。。。
非扁平依賴
對於同一npm包不同版本的管理,npmlock是非完全扁平化的處理:
所有的包的依賴順序列出來,第一次出現的包名會提升到頂層,後面重複出現的將會放入被依賴包的node_modules當中
例如下面這個例子:
第一個依賴,提升為頂層依賴
// 頂層宣告瞭loader-utils的依賴,版本為1.0.4
"loader-utils": {
"version": "1.0.4",
"resolved": "http://r.npm.sankuai.com/loader-utils/download/loader-utils-1.0.4.tgz",
"integrity": "sha1-E/Vhl/FSOjBYkSSLTHJEVAhIQmw=",
"requires": {
"big.js": "^3.1.3",
"emojis-list": "^2.0.0",
"json5": "^0.5.0"
}
}
}
對於頂級依賴滿足需求的,則不再安裝、
"sass-loader": {
"version": "7.1.0",
"resolved": "http://r.npm.sankuai.com/sass-loader/download/sass-loader-7.1.0.tgz",
"integrity": "sha1-Fv1ROMuLQkv4p1lSihly1yqtBp0=",
"dev": true,
"requires": {
// ^1.0.1 頂級依賴滿足需求
"loader-utils": "^1.0.1"
}
}
對於某些依賴不滿足的,則會在對應資料夾下面根據依賴安裝符合版本。例如less-loader
"less-loader": {
"version": "4.1.0",
"resolved": "http://r.npm.sankuai.com/less-loader/download/less-loader-4.1.0.tgz",
"requires": {
// 1.0.4 不滿足 ^1.1.0
"loader-utils": "^1.1.0",
},
"dependencies": {
"loader-utils": {
"version": "1.2.3",
"resolved": "http://r.npm.sankuai.com/loader-utils/download/loader-utils-1.2.3.tgz",
"integrity": "sha1-H/XcaRHJ8KBiUxpMBLYJQGEIwsc=",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^2.0.0",
"json5": "^1.0.1"
}
}
}
}
package-lock.json和npm-shrinkwrap.json差別
- package-lock.json不會被髮布到npm,而npm-shrinkwrap會被預設釋出
- 非頂層的package-lock.json會被忽略,而相同狀態的shrinkwrap檔案都會被保留。
- npm-shrinkwrap.json在npm 2,3,4版本均支援,package-lock.json是npm5以後引入
- 兩者同時存在,npm-shrinkwrap.json的優先順序高於package-lock.json
yarn.lcok
yarn畢竟是針對npm的缺點而生,所以其自帶版本控制,預設依賴都會生成yarn.lock檔案,該檔案會通過包名+版本來確定具體資訊。
yarn-lock語法
Yarn 用的是自己設計的格式,語法上有點像 YAML(Yarn 2.0 中將會採用標準的 YAML)。# 開頭的行是註釋。
第一行記錄了包的名稱及其語義化版本(由 package.json 定義)。
接下來的都做了縮排,表示這些是該包的資訊。
version 欄位記錄了包的確切版本。
resolved 欄位記錄了包的 URL。此外,hash 中的值是 shasum。Yarn 記錄的這個 shasum 來自於包的 versions[:version].dist.shasum(手動訪問 https://registry.npmjs.org/:package 會得到一個 JSON,解析此 JSON 可得)
dependencies 記錄了包的依賴。也許包的依賴還有依賴,但不會在這裡記錄。
如下所示:
pkg-dir@^1.0.0:
version "1.0.0"
resolved "http://r.npm.sankuai.com/pkg-dir/download/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
integrity sha1-ektQio1bstYp1EcFb/TpyTFM89Q=
dependencies:
find-up "^1.0.0"
pkg-dir@^2.0.0:
version "2.0.0"
resolved "http://r.npm.sankuai.com/pkg-dir/download/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=
dependencies:
find-up "^2.1.0"
pkg-dir@^3.0.0:
version "3.0.0"
resolved "http://r.npm.sankuai.com/pkg-dir/download/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
integrity sha1-J0kCDyOe2ZCIGx9xIQ1R62UjvqM=
dependencies:
find-up "^3.0.0"
不過Yarn 僅以 flatten 格式 描述各個包之間的依賴關係,並依賴於其當前實現來建立目錄結構。這意味著如果其內部演算法發生變化,結構也會發生變化。
提升
你會發現,有很多包你是沒有直接依賴它們的,但它們都出現在了 yarn.lock 中的頂層。這就是提升,它有兩個意義:
記錄依賴的依賴
正如上面所述,依賴的依賴不會被直接記錄在依賴的資訊下——它們會被提升,這樣可以簡化整個 yarn.lock,到時安裝依賴的時候處理也變得簡單,因為你不必一層一層的巢狀下去來查詢依賴的依賴的資訊。便於解決依賴版本衝突
依賴版本衝突是難免的,當然有時候並不是版本衝突,而只是語義化版本格式的版本記錄不同。舉個例子,^5.0.0 與 5.x.x 在很多時候並不矛盾,因此資訊可以被合併。如:
chalk@^2.0.0, chalk@^2.0.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65"
dependencies:
ansi-styles "^3.2.1"
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
注意第一行,yarn.lock 記錄了 ^2.0.0 和 ^2.0.1,而在新增 chalk 這個依賴的時候,符合語義化版本的最新版本是 2.3.2(version 欄位),這個版本對於 ^2.0.0 和 ^2.0.1 這兩個要求來說,都滿足了,因此資訊可以合併。
對於固化還是建議使用yran.lock實現,npm的lock在不同版本下存在的差異讓人頭疼。
是否應鎖版本
這個爭論是很正常的,開始使用時,我們也有過這樣的討論。
大家可以看下我們的場景再討論:
1、組內專案都依賴了自己開發的一個工具包a
2、該工具類依賴了一些第三方開源包
場景一:
當時某個知名包升級之後移除了某項功能的支援,被a依賴,導致該段時間後上線的專案全都出了問題。
場景二:
a發現出了個bug,統一修復,各個業務專案無需自行修改。
結合來看還是要具體分析,對於自行維護或者確認無誤的專案可以不鎖版本。對於第三方需要鎖版本,保證當前是可用的。對於後期的bug修復,不自行升級,對於bugfix等小版本升級,驗證完成後再次鎖版本。
.gitignore是否應該忽略lock檔案
對於 是否應該package-lock.json 不應寫進 .gitignore,可以看下賀師俊大佬的解釋:
如果你使用 lock 機制,則應該將 package-lock.json 提交到 repo 中。比如 Vue 採取了該策略。如果你不使用 lock 機制,則應該加入 .npmrc 檔案,內容為 package-lock=false ,並提交到 repo 中。比如 ESLint 採取了該策略。
還是回到了那個問題,是否應該鎖版本。
對於類庫而言,鎖定依賴版本是 絕對不可行 的。否則只要應用中使用了兩個以上的依賴,都有概率出現絕對不存在可相容版本的情況。這樣只是單純的把問題拋給了最終應用,並沒有解決問題。
最終應用是否鎖也有待考慮。
問題出在原始碼的可靠性不得到保證,本身語義化沒有問題。但是又bug正常,所以業務專案才鎖
結束語
參考文章
https://docs.npmjs.com/files/package.json
https://juejin.im/post/5ad413ba6fb9a028b5485866#heading-1
https://stackoverflow.com/questions/44258235/what-is-the-difference-between-npm-shrinkwrap-json-and-package-lock-json
針對是否應該固化版本和如何固化版本,因為水平有限也只是給出了自己的一點看法。希望能對有需要的同學有所幫助。