yarn or npm 版本固化如何選擇

瀟湘待雨發表於2019-07-10

前言

作為前端開發者,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

針對是否應該固化版本和如何固化版本,因為水平有限也只是給出了自己的一點看法。希望能對有需要的同學有所幫助。

相關文章