The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

深入理解 package-lock.json 的用途與適用情境

我最近幫一位同事整理一個已經維護 7 年的 Angular 專案,結果發現一個前後端分離的專案,只有「後端」有設定 CI / CD,前端一直以來都是人工建置與部署。我在弄 CI / CD 的時候,發現了許多 npm 套件版本有依賴問題,為了能消除這些惱人的警告,花了我好幾個小時的時間才得到徹底的解決。我這次會遇到套件依賴問題,就是因為當初的開發人員搞不定,多年來一直都視而不見,依賴問題從來都沒有搞定過,每次換人維護都用 npm install --forcenpm install --legacy-peer-deps 來解決問題,然後一代傳一代,變成祖傳的慣例,變成技術債。如果專案有個維護妥當的 package-lock.json 檔案,其實要還原套件不是什麼難事,今天這篇文章我就來深入探討這個問題。

software dependency management and stability

理解 package.json 與 package-lock.json 的差異

一般來說,任何 Node.js 的應用程式都會有個 package.json 檔案,這個檔案是用來描述專案的基本資訊、依賴套件名稱與版本、腳本等。當你執行 npm install 時,npm 會根據 package.json 中的依賴資訊來安裝相應的套件與版本。

然而 package-lock.json 是 npm 的一個鎖定檔案,主要用於記錄專案中所有安裝的套件及其精確版本。這個檔案是由 npm 在執行 npm install 時自動建立的檔案,它可以確保在不同環境中安裝相同的套件版本,從而避免因為版本不一致而導致的問題。

為了能釐清兩個檔案的差異之處,我們可以從以下幾個方面來說明:

  1. 我以 package.jsondevDependencies 區段為例:

    {
      "devDependencies": {
        "@angular/cli": "^9.1.14",
        "@angular-devkit/build-angular": "~0.901.14",
        "@angular/compiler-cli": "9.1.13"
      }
    }
    

    你從上述範例可以瞭解到,在 package.json 中定義的套件「版本」並不是一個精確的版本號,而是使用了「語意化版本」(Semantic Versioning)的方式來定義版本範圍,例如 ^9.1.14 代表可以安裝 9.x.x 的最新版本 (鎖定 MAJOR 版本),或是 ~0.901.14 代表可以安裝 0.901.x 的最新版本 (鎖定 MAJOR, MINOR 版本),而上述的 9.1.13 則代表要安裝精準的版本號,不會自動安裝新版。

    這樣的設計最主要的優點,就是在開發過程中,當有新的套件版本釋出時,可以自動獲得更新。要獲得更新,只要執行一次 npm install 即可,這樣就不需要頻繁的更新 package.json 檔案。

    npm install
    

    缺點其實也很明顯,我們都知道 Node.js 的 npm 是一個超級大黑洞,沒有人真正能理解套件與套件之間的相依性,而不同的開源套件之間也不見得會認真的維護它們的相依性相容性,這就導致了在開發過程中,當有新的套件版本釋出時,很有可能會導致專案無法正常運作,或是出現一些莫名其妙的錯誤。

    簡言之,原本 npm 可以自動更新套件的 PATCHMINOR 版本這個優點,對一個需要長年維護的專案來說,卻成為了最大的缺點,因為這樣會導致專案的相依性變得不太穩定。

  2. 如果你把 package-lock.json 檔案打開,你會發現該檔案的內容定義的非常仔細,裡面記錄了上一次執行 npm install 時,所有安裝的套件與其當下的版本資訊。

    此檔案的主要作用在於:

    • 鎖定所有依賴套件的版本,保證開發團隊成員持續整合環境部署環境安裝到的依賴樹(dependency tree)保持一致。
    • 加速套件安裝過程,因為 npm 不需每次進行重新解析每個套件之間的版本依賴。
    • 可透過 Git 版控工具提供清晰的 diff 效果,讓開發者得以檢視依賴版本的變動情形。

    簡單來說,package-lock.json 的出現,就是為了解決 package.json 的缺點,我們在 CI 的環境下,必須確保每個套件的版本不能隨便異動!

    在有 package-lock.json 的情況下,當你執行 npm ci 時,npm 會根據 package-lock.json 中的版本資訊來安裝套件,而不會根據 package.json 中的版本範圍來安裝套件。這樣可以確保在不同環境中安裝相同的套件版本,從而避免因為版本不一致而導致的問題。

    npm ci
    

    使用 npm ci 安裝套件的優點是,任何人在任何時間透過 npm ci 安裝套件,可以完整的還原出完全相同的 node_modules 目錄,所以肯定不會有版本不一致的問題。

    使用 npm ci 的缺點就是,當你在安裝套件時,如果被鎖定的套件有新的修補版本時 (Patch version),他並不會安裝新的版本。若要更新 package-lock.json 檔案,你必須手動執行 npm update 命令,這樣才能更新 package-lock.json 檔案中的版本資訊。

我該用 npm install 還是 npm ci 來安裝套件?

由於 npm installnpm ci 都是用來安裝 Node.js 專案的依賴套件,但它們的使用情境和行為有所不同。

  • npm install

    會根據 package.json 中的依賴資訊安裝套件!

    如果 package-lock.json 檔案不存在,則會自動安裝最新的相容版本,並建立一個新的 package-lock.json 檔案。

    如果 package-lock.json 檔案已存在,則會直接根據 package-lock.json 中的版本安裝套件。

    注意: 如果 package.json 中的版本範圍是 ^9.1.14,而 package-lock.json 中記錄的版本為 9.1.14,當有 9.2.0 版本釋出後,透過 npm install 安裝套件時,依然會以 9.1.14 為主要的安裝版本,這就是 package-lock.json 檔案的作用。

  • npm ci

    通常用於 CI (持續整合) 環境,會根據 package-lock.json 鎖定檔案安裝精確的版本,安裝過程也不會導致 package-lock.json 更新。這樣可以確保在 CI 環境中安裝的套件版本與本地開發環境一致,並且安裝速度更快。

    這個 npm ci 命令比較特別的地方是,如果他發現 package-lock.json 中定義的套件版本,超出了 package.json 中定義的版本範圍,則會直接報錯。這也意味著你的 package.jsonpackage-lock.json 檔案已經是無法相容的狀態,必須立刻透過 npm update 更新 package-lock.json 鎖定檔案的內容。

    簡單來說,執行 npm ci 比較容易失敗,但如果執行 npm ci 可以成功安裝套件,那肯定是比較可靠!

    注意: 如果 package.json 中的版本範圍是 ^10.0.0 (例如你手動編輯 package.json 當中的套件版本),而 package-lock.json 中記錄的版本為 9.1.14 的話(例如有團隊成員沒有將更新過的 package-lock.json 加入版控),在透過 npm ci 安裝套件時,就會因為 9.1.14^10.0.0 的版本範圍不相容而導致安裝失敗。

其實一般人就用 npm install 就好啦,如果專案中已經有 package-lock.json 檔案已存在,我們可以這樣判斷:

  1. 我不想要更新 package-lock.json 檔案的內容,而且目前 CI 可以正常透過 npm ci 命令來安裝套件。

    簡單來說,我本機的開發環境,希望能跟 CI 環境保持一致,這時候就可以使用 npm ci 命令來安裝套件。

  2. 我想要更新 package-lock.json 檔案的內容,並且目前 CI 也可以正常透過 npm ci 命令來安裝套件。

    有個穩定、可靠的 CI 環境非常關鍵,如果你的 CI 一直都可以正常運作,這時候無論你用 npm cinpm install 來安裝套件,理論上是不會更動到 package-lock.json 檔案的內容。

    但如果不是非常確定你的 package-lock.json 是否需要更新,建議還是使用 npm install 來安裝套件,如果安裝後發現了 package-lock.json 檔案有異動,那就可能意味著你的 package.json 檔案有異動過,但當時並沒有連同更新 package-lock.json 檔案。

    如果是我,可能會優先使用 npm ci 來安裝套件,讓舊專案能跑起來,比更新套件的版本重要!

    注意: 使用 npm install 有可能會直接更新 package-lock.json 檔案的內容,例如有人手動編輯了 package.json 的套件版本。

  3. 目前沒有 CI 可以驗證我的建置環境,但有 package-lock.json 檔案。

    這時候建議直接使用 npm ci 命令來安裝套件看看,如果有錯誤發生,那就代表 package-lock.json 檔案與 package.json 檔案不一致,這時候可能就需要透過 npm update 命令來更新 package-lock.json 檔案的內容。

  4. 目前沒有 CI 可以驗證我的建置環境,且 package-lock.json 檔案也不存在。

    這種情況下,你也只能用 npm install 命令來安裝套件,並且會自動建立一個新的 package-lock.json 檔案。

    當環境穩定後,還是建議你設定一個 CI 的 Pipelines,並確保在 CI 環境下可以透過 npm ci 安裝套件!

結論與建議

以下是我個人對於 package-lock.json 的看法與建議:

  1. package.json 檔案中的套件定義的版本範圍應該盡量避免人工編輯!

    盡量透過 npm install [package-name]@[version] 來安裝套件,這樣可以確保 package.json 中的版本範圍與 package-lock.json 中的版本一致。

    如果人工編輯 package.json 中的套件版本範圍,若該版本還在原本的「版本範圍」內,即便你用 npm install 安裝套件,也不見得會得到新的版本,這是因為 package-lock.json 鎖定了需要安裝的版本號。正確的更新方法應該是透過 npm update 去讀取 package.json 最新的定義,並且自動更新 package-lock.json 檔案。

  2. package-lock.json 檔案應該加入 Git 版控,且應該從 Azure Repos 或 GitHub 設定保護機制,避免任何人都可以更新這個檔案,透過 Git Hooks 也可以做到類似的效果。

    若要在 Azure DevOps 的 Pull request 檢查是否有 package-lock.json 被更新,但 package.json 卻沒有更新的狀況,可以透過 Branch policy 綁定一個 Azure Pipelines 並設定一個 Command line 執行以下命令:

    PowerShell

    # 從 Azure Pipelines 內建的環境變數取得 PR 的目標分支
    $targetBranch = $env:SYSTEM_PULLREQUEST_TARGETBRANCH
    
    # 若非 PR 建置或該變數未定義時,預設使用 origin/main,可依需求調整
    if (-not $targetBranch) {
        $targetBranch = "origin/main"
    }
    
    Write-Output "目標分支:$targetBranch"
    
    # 取得從目標分支到目前 HEAD 的變動檔案清單
    $changedFiles = git diff --name-only $targetBranch
    
    Write-Output "變更檔案:"
    $changedFiles | ForEach-Object { Write-Output $_ }
    
    # 檢查是否只更新 package-lock.json 而沒有更新 package.json
    if ($changedFiles -contains "package-lock.json" -and -not ($changedFiles -contains "package.json")) {
        Write-Output "偵測到只有 package-lock.json 更新,package.json 未更新。"
        exit 1
    } else {
        Write-Output "檢查通過。"
        exit 0
    }
    

    Bash

    #!/bin/bash
    
    # 取得 Azure Pipelines 內建的目標分支環境變數,若未定義則預設使用 origin/main
    targetBranch="${SYSTEM_PULLREQUEST_TARGETBRANCH:-origin/main}"
    echo "目標分支:${targetBranch}"
    
    # 取得從目標分支到目前 HEAD 的變更檔案清單
    changedFiles=$(git diff --name-only "$targetBranch")
    
    echo "變更檔案:"
    echo "$changedFiles"
    
    # 檢查是否只更新 package-lock.json 而沒有更新 package.json
    if echo "$changedFiles" | grep -q "^package-lock.json$" && ! echo "$changedFiles" | grep -q "^package.json$"; then
        echo "偵測到只有 package-lock.json 更新,package.json 未更新。"
        exit 1
    else
        echo "檢查通過。"
        exit 0
    fi
    
  3. 在 CI/CD 環境中,應該盡量使用 npm ci 命令來安裝套件,以確保安裝的版本與 package-lock.json 中的版本一致。

    只要 CI 的 Pipelines 執行失敗,就代表有人偷偷改過 package.json 檔案,接下來要抓鬼就比較容易些。

  4. 任何人在 git clone 複製程式碼回來後,應該先嘗試用 npm ci 安裝套件,而非使用 npm install 安裝套件。

    主要是沒有 CI 的環境下,同時又包含有 package-lock.json 檔案時,可以用 npm ci 來驗證 package-lock.json 檔案的品質。

  5. 在團隊中,應該只有少數人負責維護 package-lock.json 檔案,並提供必要的教育訓練,讓大家知道該檔案的用途。

    驗證的方法就是執行 npm install 並確認 package-lock.json 是否有被更新。

  6. 應確保只有在有套件需要新增或移除時,才需要執行 npm install 命令,並在安裝完成後,立即提交 package-lock.json 的變更。

    立即提交 package-lock.json 的變更很重要,因為這樣可以確保其他團隊成員在拉取最新的程式碼時,能夠獲得最新的依賴版本。

  7. 如果你開發的是一個函式庫套件 (Library),則可以考慮不提交 package-lock.json 至版本控制系統,以避免該函式庫的使用者因鎖定檔而遇到依賴解析不一致的問題。

案例分析

以我這次的專案遇到的問題來說,我在執行 npm install 的時候,出現了以下錯誤:

npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR!
npm ERR! While resolving: karma-jasmine-html-reporter@1.7.0
npm ERR! Found: jasmine-core@3.5.0
npm ERR! node_modules/jasmine-core
npm ERR!   dev jasmine-core@"~3.5.0" from the root project
npm ERR!   jasmine-core@"^3.5.0" from karma-jasmine@3.0.3
npm ERR!   node_modules/karma-jasmine
npm ERR!     dev karma-jasmine@"~3.0.1" from the root project
npm ERR!     peer karma-jasmine@">=1.1" from karma-jasmine-html-reporter@1.7.0
npm ERR!     node_modules/karma-jasmine-html-reporter
npm ERR!       dev karma-jasmine-html-reporter@"^1.4.2" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer jasmine-core@">=3.8" from karma-jasmine-html-reporter@1.7.0
npm ERR! node_modules/karma-jasmine-html-reporter
npm ERR!   dev karma-jasmine-html-reporter@"^1.4.2" from the root project
npm ERR!
npm ERR! Conflicting peer dependency: jasmine-core@5.6.0
npm ERR! node_modules/jasmine-core
npm ERR!   peer jasmine-core@">=3.8" from karma-jasmine-html-reporter@1.7.0
npm ERR!   node_modules/karma-jasmine-html-reporter
npm ERR!     dev karma-jasmine-html-reporter@"^1.4.2" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.

我的處理方式是:

  1. 先將訊息丟進 ChatGPT 翻譯成人話

    這個錯誤是由於 karma-jasmine-html-reporter 套件的相依版本與你目前安裝的 jasmine-core 版本不相容所導致的。具體來說,karma-jasmine-html-reporter@1.7.0 需要 jasmine-core 版本大於或等於 3.8,而你目前的專案中安裝的是 jasmine-core@3.5.0

  2. 再執行一遍 npm install,查看是否會遇到其他錯誤

    我遇到了這個錯誤:

    這個錯誤是由於 ng2-ckeditor@1.2.2 套件需要 @angular/common 版本在 7.x.x 範圍內,但你的專案目前使用的是 @angular/common@9.1.13,因此會出現版本衝突。

    這訊息就沒有很明確了,鬼才知道 ng2-ckeditor 到底有沒有支援 Angular 9.1.13 版本,我去了這個套件的 Repo 查看發行記錄,什麼都沒寫。

  3. 嘗試解決套件版本衝突問題

    首先,我在 package.json 有看到宣告的版本為 ^1.2.2,這代表 1.x.x 都是升版的範圍。而我在 package-lock.json 查到的版本是 1.2.2,這意味著當你執行 npm install 的時候,永遠只會安裝 1.2.2 版本,所以永遠無法與 @angular/common@9.1.13 相容。

    為了解決版本鎖定問題,我選擇將 package-lock.json 檔案刪除,然後重新執行 npm install,這樣就可以安裝到最新的 ng2-ckeditor 套件版本了。經過上述調整,確實版本衝突問題已解決。

  4. 再執行一遍 npm install,查看是否會遇到其他錯誤

    我遇到了這個錯誤:

    npm ERR! code 1
    npm ERR! path G:\Projects\my-project\node_modules\node-sass
    npm ERR! command failed
    npm ERR! command C:\Program Files\git\bin\bash.exe -c -- node scripts/build.js
    npm ERR! Building: C:\Program Files\nodejs\node.exe G:\Projects\my-project\node_modules\node-gyp\bin\node-gyp.js rebuild --verbose --libsass_ext= --libsass_cflags= --libsass_ldflags= --libsass_library=
    npm ERR! Building the projects in this solution one at a time. To enable parallel build, please add the "-m" switch.
    npm ERR! Build started 2025/3/27 下午 04:43:43.
    npm ERR! Project "G:\Projects\my-project\node_modules\node-sass\build\binding.sln" on node 1 (default targets).
    npm ERR! ValidateSolutionConfiguration:
    npm ERR!   Building solution configuration "Release|x64".
    npm ERR! Project "G:\Projects\my-project\node_modules\node-sass\build\binding.sln" (1) is building "G:\Projects\my-project\node_modules\node-sass\build\binding.vcxproj.metaproj" (2) on node 1 (default targets).
    npm ERR! Project "G:\Projects\my-project\node_modules\node-sass\build\binding.vcxproj.metaproj" (2) is building "G:\Projects\my-project\node_modules\node-sass\build\src\libsass.vcxproj" (3) on node 1 (default targets).
    npm ERR! C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\15.0\Bin\Microsoft\VC\v160\Microsoft.CppBuild.targets(439,5): error MSB8020: The build tools for Visual Studio 2017 (Platform Toolset = 'v141') cannot be found. To build using the v141 build tools, please install Visual Studio 2017 build tools.  Alternatively, you may upgrade to the current Visual Studio tools by selecting the Project menu or right-click the solution, and then selecting "Retarget solution". [G:\Projects\my-project\node_modules\node-sass\build\src\libsass.vcxproj]
    npm ERR! Done Building Project "G:\Projects\my-project\node_modules\node-sass\build\src\libsass.vcxproj" (default targets) -- FAILED.
    npm ERR! Done Building Project "G:\Projects\my-project\node_modules\node-sass\build\binding.vcxproj.metaproj" (default targets) -- FAILED.
    npm ERR! Done Building Project "G:\Projects\my-project\node_modules\node-sass\build\binding.sln" (default targets) -- FAILED.
    npm ERR!
    npm ERR! Build FAILED.
    

    我一樣將訊息丟進 ChatGPT 翻譯成人話:

    這個錯誤表明在安裝 node-sass 時,編譯過程中出現了問題,具體來說是因為缺少 Visual Studio 2017 的建構工具。node-sass 需要在 Windows 上編譯 C++ 原生代碼,並且依賴 Visual Studio 的特定版本來進行編譯。你的錯誤訊息指出,當前的 Visual Studio 配置缺少必要的 v141 平台工具集。

  5. 嘗試解決無法建置的問題

    雖然安裝 Visual Studio 2017 Build Tools 也可以解決問題,但這解決方案並不完美,因為無法跨平臺。

    解決這個問題需要靠經驗了,由於 node-sass 在較舊的版本中常常會遇到安裝問題,我已經遇過太多次了。建議改用 sass (Dart-sass) 作為替代品,這個套件不需要編譯過程,可以避免很多編譯問題,跨平臺也能用。

    我的解決方法是,先手動編輯 package.json 檔案,移除 node-sass 套件。然後手動執行以下命令,安裝 sass 套件最新版:

    npm install sass --save-dev
    

    手動編輯 package.json 檔案並移除 node-sass 套件,最主要的考量就是減少等待 npm uninstall node-sass 的時間。而且如果有其他依賴問題存在的話,你可能執行 npm uninstall node-sass 也會遇到錯誤,那就很悲劇了。

上述案例正是我這次解決問題的完整過程與思路,希望對大家有幫助!😊

相關連結

留言評論