npm 與 yarn 安裝包不一致

時傾發表於2021-10-26

問題

run 一個 CRA 專案,使用 npm 與 yarn 安裝包,發現 npm 安裝的包有 @babel/plugin-proposal-optional-chaining, 而 yarn 安裝的沒有 @babel/plugin-proposal-optional-chaining。本地 npm 安裝包後啟動正常,而生產環境使用的 yarn, 造成構建失敗。

原因

yarn install 安裝生成的 yarn.lock 檔案 與 npm install 生成的 package-lock.json 檔案時間相差較遠,造成了 yarn.lock 的包版本低於 package-lock.json 的包版本。因為以 ˆx.x.x 形式定義的包版本在不同時期安裝包版本不一致。

yarn 安裝 @babel/preset-env 版本有: "7.5.5", "^7.4.5",實際安裝的 version 是 "7.5.5"。
image.png

npm 安裝 @babel/preset-env 版本有: "7.9.0", "^7.4.5",實際安裝的 version 是 "7.9.0" 和 "7.11.5"。而在 “7.8.3”版本里首次依賴 @babel/plugin-proposal-optional-chaining
image.png

解決:如果執行 yarn upgeade 就會更新 yark.lock 檔案, 獲取最新的包版本。

擴充

package.json

package.json 主要用來記錄依賴包名稱、版本、執行指令等資訊欄位。
其中,dependencies 欄位指定了專案執行所依賴的模組,devDependencies 指定專案開發所需要的模組。它們都指向一個物件,該物件的各個成員,分別由模組名和對應的版本組成,表示依賴的模組及其版本範圍。

版本限定方式【語義化版本】,主要有以下幾種:

  • 指定版本:比如 1.2.2 ,遵循“大版本.次要版本.小版本”的格式規定,安裝時只安裝指定版本。
  • 波浪號(tilde)+指定版本:比如 ~1.2.2 ,表示安裝 1.2.x 的最新版本(不低於1.2.2),但是不安裝 1.3.x,也就是說安裝時不改變大版本號和次要版本號。
  • 插入號(caret)+指定版本:比如 ˆ1.2.2,表示安裝 1.x.x 的最新版本(不低於 1.2.2),但是不安裝 2.x.x,也就是說安裝時不改變大版本號。需要注意的是,如果大版本號為 0,則插入號的行為與波浪號相同,這是因為此時處於開發階段,即使是次要版本號變動,也可能帶來程式的不相容。
  • latest:安裝最新版本。

當我們使用比如 npm install package -save 安裝一個依賴包時,版本是插入號形式。這樣每次重新安裝依賴包 npm install 時”次要版本“和“小版本”是會拉取最新的。一般的,主版本不變的情況下,不會帶來核心功能變動,API 應該相容舊版。

package-lock.json

在 npm5 版本後,執行 npm intall 會生成一個新檔案 package-lock.json。
當專案中已有 package-lock.json 檔案,在安裝專案依賴時,將以該檔案為主進行解析安裝指定版本依賴包,而不是使用 package.json 來解析和安裝模組。因為 package-lock 為每個模組及其每個依賴項指定了版本,位置和完整性雜湊,所以它每次建立的安裝都是相同的。 無論你使用什麼裝置,或者將來安裝它都無關緊要,每次都應該給你相同的結果。

yarn

yarn 的出現主要目標是解決由於語義版本控制而導致的 npm 安裝的不確定性問題。
每個 yarn 安裝都會生成一個類似於 npm-shrinkwrap.json 的 yarn.lock 檔案,而且它是預設建立的。除了常規資訊之外,yarn.lock 檔案還包含要安裝的內容的校驗和,以確保使用的庫的版本相同。

yarn 的主要優化

  • 並行安裝:無論 npm 還是 yarn 在執行包的安裝時,都會執行一系列任務。npm 是按照佇列執行每個 package,也就是說必須要等到當前 package 安裝完成之後,才能繼續後面的安裝。而 yarn 是同步執行所有任務,提高了效能。
  • 離線模式:如果之前已經安裝過一個軟體包,用 yarn 再次安裝時之間從快取中獲取,就不用像 npm 那樣再從網路下載了。
  • 安裝版本統一:為了防止拉取到不同的版本,yarn 有一個鎖定檔案 (lock file) 記錄了被安裝上的模組的版本號。每次只要新增了一個模組,yarn 就會建立(或更新)yarn.lock 這個檔案。這麼做就保證了,每一次拉取同一個專案依賴時,使用的都是一樣的模組版本。
  • 更好的語義化: yarn 改變了一些 npm 命令的名稱,比如 yarn add/remove,比 npm 原本的 install/uninstall 要更清晰。

安裝依賴樹流程

  1. 執行工程自身 preinstall
    如果定義了 preinstall 鉤子此時會被執行。
  2. 確定首層依賴
    首先需要做的是確定專案中的首層依賴,也就是 dependencies 和 devDependencies 屬性中直接指定的模組(假設此時沒有新增 npm install 引數)。
    專案本身是整棵依賴樹的根節點,每個首層依賴模組都是根節點下面的一棵子樹,npm 會開啟多程式從每個首層依賴模組開始逐步尋找更深層級的節點。
  3. 下載模組
    下載模組是一個遞迴的過程,分為以下幾步:

    • 獲取模組版本資訊。在下載一個模組之前,首先要確定其版本,這是因為 package.json 中往往是語義化版本。
      如果版本描述檔案(npm-shrinkwrap.json 或 package-lock.json)中有該模組資訊直接用即可,如果沒有則從倉庫獲取。如 package.json 中某個包的版本是 ^1.1.0,npm 就會去倉庫中獲取符合 1.x.x 形式的最新版本。
    • 獲取模組實體。上一步會獲取到模組的壓縮包地址(resolved 欄位),npm 會用此地址檢查本地快取,快取中有就直接拿,如果沒有則從倉庫下載。
    • 查詢該模組依賴,如果有依賴則回到第 1 步,如果沒有則停止。
  4. 模組扁平化(dedupe)。
    上一步獲取到的是一棵完整的依賴樹,其中可能包含大量重複模組。比如 A 模組依賴於 loadsh,B 模組同樣依賴於 lodash。在 npm3 以前會嚴格按照依賴樹的結構進行安裝,因此會造成模組冗餘。yarn 和從 npm5 開始預設加入了一個 dedupe 的過程,它會遍歷所有節點,逐個將模組放在根節點下面,也就是 node-modules 的第一層。當發現有重複模組時,則將其丟棄。
重複模組
它指的是模組名相同且語義化相容。每個語義化版本都對應一段版本允許範圍,如果兩個模組的版本允許範圍存在交集,那麼就可以得到一個相容版本,而不必版本號完全一致,這可以使更多冗餘模組在 dedupe 過程中被去掉。
  1. 安裝模組
    這一步將會更新工程中的 node_modules,並執行模組中的生命週期函式(按照 preinstall、install、postinstall 的順序)。
  2. 執行專案自身生命週期
    當前 npm 工程如果定義了鉤子此時會被執行(按照 install、postinstall、prepublish、prepare 的順序)

安裝依賴例項

外掛 htmlparser2@^3.10.1dom-serializer@^0.2.2 都有使用了 entities 依賴包,不過使用的版本不同,同時我們自己安裝一個版本的 entities 包。具體如下:

--htmlparser2@^3.10.1
  |--entities@^1.1.1

--dom-serializer@^0.2.2
  |--entities@^2.0.0

--entities@^2.1.0

通過 npm install 安裝後,生成的 package-lock.json 檔案內容和它的 node_modules 目錄結構:

npm-version

可以發現:

dom-serializer@^0.2.2 的依賴包 entities@^2.0.0 和我們自己安裝的 entities@^2.1.0 被實際安裝成 entities@^2.2.0,並放在 node_modules 的第一層。因為這兩個版本的semver 範圍相同,又先被遍歷,所有會被合併安裝在第一層;
htmlparser2@^3.10.1 的依賴包 entities@^1.1.1 被實際安放在 dom-serializer 包的 node_modules 中,並且和 package-lock.json 描述結構保持一致。
通過 yarn 安裝後,生成的 yarn.lock 檔案內容和它的 node_modules 目錄結構:

yarn-version

可以發現與 npm install 不同的是:

yarn.lock 中所有依賴描述都是扁平化的,即沒有依賴描述的巢狀關係;
在 yarn.lock 中, 相同名稱版本號不同的依賴包,如果 semver 範圍相同會被合併,否則,會存在多個版本描述。

參考: npm/yarn lock真香

相關文章