工具大全
开发者工具作者:Coocon2026年9月12日3 次阅读约 4 分钟阅读

npm install 会改你的 package-lock.json:CI 必须用 npm ci 的 3 个理由

问一个问题:npm installnpm ci 有什么区别?

大多数人的答案是「ci 快一点,CI 里用」。这个答案不算错,但漏掉了最关键的一条:npm install 有权改写 package-lock.jsonnpm ci 永远不会。 一个是「按 lock 装,装不上就改 lock」,一个是「按 lock 装,装不上就报错」。

这一字之差,在你本机上几乎感觉不到。在 CI 里,它决定了你的构建到底是不是可复现的。

先说清楚 npm ci 做了什么

npm ci 里的 ci 不是 continuous integration,官方叫它 clean install。它的行为是固定的四条:

  1. 必须有 package-lock.json(或 npm-shrinkwrap.json),没有直接报错。
  2. package.json 和 lock 不一致就报错退出,不会替你修。
  3. 先删掉整个 node_modules,再从 lock 里逐条安装。
  4. 绝不写 package-lock.json

对比 npm install:没有 lock 就生成一份;lock 和 package.json 对不上就重新解析、把结果写回 lock;node_modules 里已有的包能复用就复用。

所以 npm install 是「让项目能跑起来」,npm ci 是「验证 lock 文件描述的就是能跑起来的项目」。前者适合你写代码,后者适合机器验收。

下面三个场景,每一个我都见过团队因此浪费半天。

理由一:lock 文件会被镜像源来回翻转

打开你的 package-lock.json,每个包下面有一行 resolved

"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"

这个 URL 是从当时安装用的 registry 来的。国内团队常见的情况是:A 同学 .npmrc 配了 npmmirror,B 同学用官方源,C 同学公司内网有私有仓库。三个人各自 npm install 一次,lock 文件里几百行 resolved 就换一次域名,diff 里全是无意义的 URL 变更,真正的版本变化淹没在里面。

npm ci 在这一步不会写 lock,所以 CI 不会制造这种 diff。但本地开发还是会。彻底的解法是把 registry 写进项目根目录的 .npmrc 并提交:

# .npmrc(提交到仓库)
registry=https://registry.npmmirror.com

所有人、包括 CI,用同一个源,resolved 就不再翻转。较新版本的 npm 还有一个 replace-registry-host 配置项,可以在安装时把 lock 里的 registry 域名替换成当前配置的源,适合已经翻乱了的老仓库。

理由二:macOS 生成的 lock,在 Linux 上装不上

这一条最反直觉,也最常见。

esbuild、swc、rollup、sharp 这类带原生二进制的包,把不同平台的二进制拆成了各自的可选依赖,比如 @rollup/rollup-darwin-arm64@rollup/rollup-linux-x64-gnu。你在 Apple Silicon 的 Mac 上 npm install,npm 只会把 darwin-arm64 那个装进 node_modules

问题出在旧版本 npm 的一个已知缺陷上:某些情况下它把其他平台的可选依赖从 lock 文件里漏掉了。lock 提交上去,CI 在 Linux 上跑 npm ci,按 lock 逐条安装,没有 linux-x64 那一条,构建时直接报:

Error: Cannot find module @rollup/rollup-linux-x64-gnu

npm install 在 CI 上反而不会报错,因为它发现缺了就重新解析、补上,顺便又改了一遍 lock。这就是很多人「本地和 CI 都用 install 一直没事,换成 ci 就炸」的原因。不是 npm ci 有问题,是它诚实地告诉你 lock 文件不完整。

修法两步:

# 1. 删掉不完整的 lock 和 node_modules,重新生成
rm -rf node_modules package-lock.json
npm install

# 2. 确认 lock 里包含了其他平台的条目
grep -c "rollup-linux-x64-gnu" package-lock.json

npm 9 以后的版本可以在安装时显式声明目标平台,让 lock 一开始就带上:

npm install --os=linux --cpu=x64

如果你的团队全是 Mac、CI 全是 Linux,把 lock 的重新生成放在 Linux 容器里做一次,之后就不会再遇到。

理由三:手改 package.json 之后,lock 静默失步

有人图省事,直接在 package.json 里把 "axios": "^1.6.0" 改成 "^1.7.0",然后提交。本地跑了 npm install,npm 发现不一致,重新解析,lock 更新了,一切正常。但如果这个人没跑 install 就提交了,或者跑了但没把 lock 一起提交,仓库里就出现了 package.json 和 lock 不一致的状态。

这种状态下:

  • CI 用 npm install:静默按 package.json 重新解析,装上 1.7.x,构建通过,但装的版本和任何人本地都不一样。
  • CI 用 npm ci:立刻报错。
npm ERR! `npm ci` can only install packages when your package.json and
package-lock.json are in sync. Please update your lock file with `npm install`
before continuing.

第二种才是你想要的。lock 文件的全部意义是「所有环境装同一套东西」,一旦允许 CI 自行解析,这个承诺就没了。

一张表:三个包管理器的对应命令

目的 npm yarn 1.x yarn 2+ pnpm
开发时安装 npm install yarn yarn pnpm install
CI 严格安装 npm ci yarn --frozen-lockfile yarn install --immutable pnpm install --frozen-lockfile
lock 不一致时 报错 报错 报错 报错
生产环境不装 devDependencies npm ci --omit=dev yarn --production 需配置 pnpm install --prod

pnpm 有一个细节值得知道:它检测到 CI 环境变量时,pnpm install 默认就是 --frozen-lockfile 行为。也就是说在 GitHub Actions 里你不加参数也是严格模式。npm 没有这个默认,必须显式写 npm ci

今天就能改的三处

GitHub Actions 里:

- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: npm        # 缓存 ~/.npm,npm ci 每次删 node_modules 也不慢
- run: npm ci

Dockerfile 里:

COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .

先拷 lock 再 npm ci,这一层只在 lock 变化时重建,缓存命中率比 COPY . . 之后再 install 高得多。

本地 git hook 里(可选,防理由三):

# .husky/pre-commit
npm ls --depth=0 > /dev/null 2>&1 || {
  echo "package.json 与 package-lock.json 不一致,先跑 npm install"
  exit 1
}

一句话总结

npm install 是给人用的,它会替你把事情做对;npm ci 是给机器用的,它只负责告诉你事情对不对。CI 的职责是验证,不是修复。 把 CI 里的 install 换成 ci,你会多收到几次报错,每一次都是它替你拦下了一个「本地能跑、线上不能」的问题。

顺便问一句:你们的 CI 现在用的是哪个?评论区站个队。

相关文章

86 分钟:Rust 供应链攻击把 yank 机制变成了诱饵

2026 年 8 月 20 日,arrayref 被投毒。真正值得研究的不是恶意代码本身,而是攻击者把 cargo 的 yank 提示变成了社会工程武器——先把好版本全部标记为废弃,再让 cargo 亲口建议你升级到那个恶意版本。整条链路只在线 86 分钟,却踩中了 Rust 依赖模型里最难防的一环:build.rs 在编译期就是任意代码执行。

rustcargo+4
developer2026年8月21日8 min
206

Node.js 内置 fetch 不走 HTTPS_PROXY:Google API 连不上的第二个坑

curl 带代理能通,Node 脚本里的 fetch 却一直超时?Node.js 内置 fetch(undici)设计上就不读 HTTPS_PROXY 等环境变量。本文用 Google Search Console API 的实际排查过程讲清这个行为,并给出三种修复路径:google-auth-library 的 client.request()、undici ProxyAgent、以及新版 Node 的实验性开关。

troubleshootingproxy+4
developer2026年8月5日3 min
168

GA4 Data API 代理环境超时 DEADLINE_EXCEEDED:gRPC 不认大写 HTTPS_PROXY

GA4 Data API 在代理环境下 60 秒超时报 DEADLINE_EXCEEDED,而 curl 走同一个代理完全正常。根因是 SDK 底层走 gRPC,而 grpc-js 只读小写的 grpc_proxy / https_proxy,大写 HTTPS_PROXY 对它不可见。本文从报错现象到源码逐层排查,给出一次配对的修复方案。

troubleshootingga4+3
developer2026年8月5日3 min
122

私有仓库的文章如何自动同步到公开镜像站:GitHub Actions 跨仓库推送方案

如果你有一个私有 GitHub 仓库存放代码和文章,但又希望文章内容公开可访问,可以通过公开镜像仓库 + GitHub Actions 自动同步来实现。本文记录了一次完整的实施过程,包括跨仓库 Token 配置、同步工作流设计和常见注意事项。

cicdgithub-actions+4
developer2026年7月23日4 min
225