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

理解 package.json 與 package-lock.json 的差異
一般來說,任何 Node.js 的應用程式都會有個 package.json
檔案,這個檔案是用來描述專案的基本資訊、依賴套件名稱與版本、腳本等。當你執行 npm install
時,npm 會根據 package.json
中的依賴資訊來安裝相應的套件與版本。
然而 package-lock.json
是 npm 的一個鎖定檔案,主要用於記錄專案中所有安裝的套件及其精確版本。這個檔案是由 npm 在執行 npm install
時自動建立的檔案,它可以確保在不同環境中安裝相同的套件版本,從而避免因為版本不一致而導致的問題。
為了能釐清兩個檔案的差異之處,我們可以從以下幾個方面來說明:
-
我以 package.json
的 devDependencies
區段為例:
{
"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
可以自動更新套件的 PATCH
或 MINOR
版本這個優點,對一個需要長年維護的專案來說,卻成為了最大的缺點,因為這樣會導致專案的相依性變得不太穩定。
-
如果你把 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 install
和 npm 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.json
與 package-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
檔案已存在,我們可以這樣判斷:
-
我不想要更新 package-lock.json
檔案的內容,而且目前 CI 可以正常透過 npm ci
命令來安裝套件。
簡單來說,我本機的開發環境,希望能跟 CI 環境保持一致,這時候就可以使用 npm ci
命令來安裝套件。
-
我想要更新 package-lock.json
檔案的內容,並且目前 CI 也可以正常透過 npm ci
命令來安裝套件。
有個穩定、可靠的 CI 環境非常關鍵,如果你的 CI 一直都可以正常運作,這時候無論你用 npm ci
或 npm 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
的套件版本。
-
目前沒有 CI 可以驗證我的建置環境,但有 package-lock.json
檔案。
這時候建議直接使用 npm ci
命令來安裝套件看看,如果有錯誤發生,那就代表 package-lock.json
檔案與 package.json
檔案不一致,這時候可能就需要透過 npm update
命令來更新 package-lock.json
檔案的內容。
-
目前沒有 CI 可以驗證我的建置環境,且 package-lock.json
檔案也不存在。
這種情況下,你也只能用 npm install
命令來安裝套件,並且會自動建立一個新的 package-lock.json
檔案。
當環境穩定後,還是建議你設定一個 CI 的 Pipelines,並確保在 CI 環境下可以透過 npm ci
安裝套件!
結論與建議
以下是我個人對於 package-lock.json
的看法與建議:
-
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
檔案。
-
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
-
在 CI/CD 環境中,應該盡量使用 npm ci
命令來安裝套件,以確保安裝的版本與 package-lock.json
中的版本一致。
只要 CI 的 Pipelines 執行失敗,就代表有人偷偷改過 package.json
檔案,接下來要抓鬼就比較容易些。
-
任何人在 git clone
複製程式碼回來後,應該先嘗試用 npm ci
安裝套件,而非使用 npm install
安裝套件。
主要是沒有 CI 的環境下,同時又包含有 package-lock.json
檔案時,可以用 npm ci
來驗證 package-lock.json
檔案的品質。
-
在團隊中,應該只有少數人負責維護 package-lock.json
檔案,並提供必要的教育訓練,讓大家知道該檔案的用途。
驗證的方法就是執行 npm install
並確認 package-lock.json
是否有被更新。
-
應確保只有在有套件需要新增或移除時,才需要執行 npm install
命令,並在安裝完成後,立即提交 package-lock.json
的變更。
立即提交 package-lock.json
的變更很重要,因為這樣可以確保其他團隊成員在拉取最新的程式碼時,能夠獲得最新的依賴版本。
-
如果你開發的是一個函式庫套件 (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.
我的處理方式是:
-
先將訊息丟進 ChatGPT 翻譯成人話
這個錯誤是由於 karma-jasmine-html-reporter
套件的相依版本與你目前安裝的 jasmine-core
版本不相容所導致的。具體來說,karma-jasmine-html-reporter@1.7.0
需要 jasmine-core
版本大於或等於 3.8
,而你目前的專案中安裝的是 jasmine-core@3.5.0
。
-
再執行一遍 npm install
,查看是否會遇到其他錯誤
我遇到了這個錯誤:
這個錯誤是由於 ng2-ckeditor@1.2.2
套件需要 @angular/common
版本在 7.x.x
範圍內,但你的專案目前使用的是 @angular/common@9.1.13
,因此會出現版本衝突。
這訊息就沒有很明確了,鬼才知道 ng2-ckeditor 到底有沒有支援 Angular 9.1.13 版本,我去了這個套件的 Repo 查看發行記錄,什麼都沒寫。
-
嘗試解決套件版本衝突問題
首先,我在 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
套件版本了。經過上述調整,確實版本衝突問題已解決。
-
再執行一遍 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 平台工具集。
-
嘗試解決無法建置的問題
雖然安裝 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
也會遇到錯誤,那就很悲劇了。
上述案例正是我這次解決問題的完整過程與思路,希望對大家有幫助!😊
相關連結