先看出事的那一刻
2026 年 9 月 27 日,这个网站迁到 Astro 的分支推了上去。本地构建是好的,28 个页面全部生成。同一次提交上,Vercel 构建成功,Cloudflare Pages 构建失败;第 1 课讲过,后者才是真正的线上。
打开构建日志,关键的几行说了两件事:一个警告,一个错误。
Detected the following tools from environment: npm@10.9.2, nodejs@22.16.0
npm warn EBADENGINE package: 'undici@8.11.2',
npm warn EBADENGINE required: { node: '>=22.19.0' },
npm error `npm ci` can only install packages when your package.json and package-lock.json ... are in sync.
npm error Missing: @emnapi/core@1.11.3 from lock file
npm error Missing: @emnapi/runtime@1.11.3 from lock file
这节课要回答的是:本地构建通过、线上却失败,差在哪里?怎样让每台机器装出同一套依赖、跑在同一个版本上?
装备几个词
为什么要懂
AI 替你做了
AI 会替你装依赖、升级版本、生成锁文件,本地一跑就通,还会告诉你“构建成功”。
留给你的
确认线上的构建机器和你的电脑是不是同一个环境:同一个包管理器版本、同一个 Node 版本、同一份锁文件。
不懂的代价
本地一切正常,一推送就失败,报错还指向你从没听说过的包;更糟的是,在本地怎么查都查不出来。
一张清单,一本账,一台机器
要让任何一台机器都装出同一套东西,得有三样:
- 清单:
package.json,写着项目直接用到哪些依赖、可以接受的版本范围。这个网站的清单上只有 11 个。 - 账本:锁文件,记下这一次实际装上的每个包的确切版本,包括依赖的依赖。这个网站的锁文件里记着 781 个包。
- 机器:运行它们的 Node,和负责安装的包管理器,各有各的版本。
三样里只要有一样不同,结果就可能不同。这一次,三样全不一样:
| 本地 | Cloudflare 构建机 | |
|---|---|---|
| npm | 11.6 | 10.9 |
| Node | 25.2 | 22.16 |
| 安装命令 | npm install,会顺手修补锁文件 | npm ci,锁文件对不上就退出 |
为什么在本地查不出来
npm 11 生成的锁文件里,有一个 WebAssembly 版本的编译器包。它在锁文件里声明依赖 @emnapi/core 和 @emnapi/runtime,这两个包自己却没有条目:清单上写了"要这两样",账本里却没记。
这个编译器包只在特定的处理器架构上才会安装,苹果电脑上根本不装。所以在本地运行 npm ls @emnapi/core,结果是空的,没有任何迹象。直到构建机上的 npm 10 用严格模式逐条核对账本,才发现少了这两行。
那条 Node 版本的警告,来自一个隔了三层的间接依赖:
astro@7.3.5 → unifont@0.7.5 → undici@8.11.2 (要求 Node 22.19 以上)
项目的 package.json 其实写了版本要求,但写的是 22.12 以上,已经过时;而且构建平台选 Node 版本,看的不是它。
深潜修法:让线上和本地对齐+
echo "24" > .node-version # 告诉构建平台用 Node 24
# package.json 里:"engines": { "node": ">=22.19.0" } # 声明真实的最低版本
npx -y npm@10 install --package-lock-only --no-audit --no-fund # 用和线上一样的 npm 10 重新生成锁文件
然后在一份干净的副本里,模拟线上执行 npx -y npm@10 ci 和构建:装了 615 个包,28 个页面全部生成,确认通过才推送。这次补上了锁文件里缺的 8 个条目,没有升级任何依赖。
最后,把"锁文件请用 npm 10 生成"写进了 README 的部署一节。这条规矩只写在文档里:package.json 里没有锁定包管理器版本的字段,下一个人换台电脑,还可能再踩一次。
深潜engines 和 .node-version 有什么区别+
package.json 里的 engines 是一个声明:"这个项目至少需要这个版本"。npm 看到版本不够会发警告,但不会替你换版本。
.node-version(或 .nvmrc)是写给工具看的指令:"请用这个版本来跑"。Cloudflare Pages 这类平台构建时会读它,没有它,平台就用自己的默认版本。
两者最好都写:一个管底线,一个管实际用哪个。
找茬
下面是那次失败的构建日志,以及当时项目里关于 Node 版本的两处说明(节选并略作简化)。找出指向问题根源的行:哪些信息说明了环境不一致、依赖要求没满足、配置已经过时。
这段 构建日志 / JSON 里埋了 5 处问题。点击你觉得有问题的行,至少找出 3 处再揭晓。
·第 2 行中危
构建机的版本,和你的电脑不一样
构建机用的是 npm 10.9 和 Node 22.16,本地是 npm 11.6 和 Node 25.2。版本不同,是这次一切问题的前提。日志第一行就写着,很多人会直接跳过。
该问的话:线上构建用的是哪个版本的 npm 和 Node?和我本地一样吗?
·第 4 行中危
依赖要的 Node 版本,比构建机的高
一个隔了三层的间接依赖要求 Node 22.19 以上。这只是一条警告,不拦构建,但运行时用到新版本才有的功能就会出错。
该问的话:这个依赖是被谁带进来的?项目需要的最低 Node 版本是多少?
·第 7 行高危
锁文件里少了条目:这才是失败的原因
npm ci严格按锁文件安装,发现锁文件里声明了依赖、却找不到它的条目,就直接退出。缺的这个包只在另一种处理器上才会安装,所以在本地怎么查都查不到。该问的话:锁文件是用哪个版本的 npm 生成的?用线上的版本重新生成,会有什么变化?
·第 10 行中危
声明的最低版本已经过时
engines写着 22.12 以上,可依赖早就要求 22.19 了。而且构建平台选 Node 版本看的是.node-version这类文件,项目里没有,平台就用了自己的默认值。该问的话:项目里有没有告诉构建平台用哪个 Node 版本的文件?
·第 11 行低危
文档里的要求跟着一起过时
README 也写着 22.12,还标注了"Astro 7 的要求"。要求变了,文档没变,下一个照着文档配环境的人,会再踩一次。
该问的话:README 里的环境要求,和 package.json、.node-version 一致吗?
查看代码与答案(5 处问题)
1 # Cloudflare Pages 构建日志(2026-09-27,节选并略作简化)
2 Detected the following tools from environment: npm@10.9.2, nodejs@22.16.0
3 Installing project dependencies: npm clean-install --progress=false
4 npm warn EBADENGINE Unsupported engine { package: 'undici@8.11.2', required: { node: '>=22.19.0' }, current: { node: 'v22.16.0', npm: '10.9.2' } }
5 npm error code EUSAGE
6 npm error `npm ci` can only install packages when your package.json and package-lock.json are in sync.
7 npm error Missing: @emnapi/core@1.11.3 from lock file
8
9 # 迁移时的 package.json 与 README(节选)
10 "engines": { "node": ">=22.12.0" }
11 需要 Node 22.12 及以上(Astro 7 的要求)。- 第 2 行 · 中危 · 构建机的版本,和你的电脑不一样:构建机用的是 npm 10.9 和 Node 22.16,本地是 npm 11.6 和 Node 25.2。版本不同,是这次一切问题的前提。日志第一行就写着,很多人会直接跳过。 该问的话:线上构建用的是哪个版本的 npm 和 Node?和我本地一样吗?
- 第 4 行 · 中危 · 依赖要的 Node 版本,比构建机的高:一个隔了三层的间接依赖要求 Node 22.19 以上。这只是一条警告,不拦构建,但运行时用到新版本才有的功能就会出错。 该问的话:这个依赖是被谁带进来的?项目需要的最低 Node 版本是多少?
- 第 7 行 · 高危 · 锁文件里少了条目:这才是失败的原因:
npm ci严格按锁文件安装,发现锁文件里声明了依赖、却找不到它的条目,就直接退出。缺的这个包只在另一种处理器上才会安装,所以在本地怎么查都查不到。 该问的话:锁文件是用哪个版本的 npm 生成的?用线上的版本重新生成,会有什么变化? - 第 10 行 · 中危 · 声明的最低版本已经过时:
engines写着 22.12 以上,可依赖早就要求 22.19 了。而且构建平台选 Node 版本看的是.node-version这类文件,项目里没有,平台就用了自己的默认值。 该问的话:项目里有没有告诉构建平台用哪个 Node 版本的文件? - 第 11 行 · 低危 · 文档里的要求跟着一起过时:README 也写着 22.12,还标注了"Astro 7 的要求"。要求变了,文档没变,下一个照着文档配环境的人,会再踩一次。 该问的话:README 里的环境要求,和 package.json、.node-version 一致吗?
快测
1. 本地 npm install 之后能构建,线上 npm ci 报“锁文件不同步”。最可能的原因是?
你可能是这么想的:Node 版本不够只是一条警告(日志里的 EBADENGINE),不拦安装;拦住这次构建的,是 npm ci 报的“锁文件不同步”。
你可能是这么想的:报错写的是 Missing … from lock file,缺的是锁文件里的条目,不是清单里少写了依赖。
对了。npm ci 严格按锁文件安装,对不上就直接退出。这次是 npm 11 生成的锁文件缺了两个包的条目,构建机上的 npm 10 逐条核对时才发现。
锁文件的问题,要用和线上相同的包管理器版本来复现。
查看选项与答案
- A. 线上 Node 版本低于依赖的要求,安装因此被拦下——Node 版本不够只是一条警告(日志里的
EBADENGINE),不拦安装;拦住这次构建的,是npm ci报的“锁文件不同步”。 - B.
package.json漏写了一个依赖,线上因此装不全——报错写的是 Missing … from lock file,缺的是锁文件里的条目,不是清单里少写了依赖。 - C. 生成锁文件的 npm 版本和线上不同,锁文件里缺了条目(正确)——
npm ci严格按锁文件安装,对不上就直接退出。这次是 npm 11 生成的锁文件缺了两个包的条目,构建机上的 npm 10 逐条核对时才发现。
锁文件的问题,要用和线上相同的包管理器版本来复现。
2. package.json 里写了 "engines": { "node": ">=22.19.0" },构建平台就会自动用 22.19 以上的 Node 吗?
对了。声明底线和指定版本是两件事:engines 管底线,.node-version 告诉平台实际用哪个,最好都写。没有它,平台就用自己的默认版本。
你可能是这么想的:版本不够时,npm 只发一条警告,不会中止安装,更不会替你换版本。这次日志里的 EBADENGINE 就只是警告,构建照样往下走。
你可能是这么想的:engines 只是声明,npm 据此发警告;平台选版本,多半看 .node-version、.nvmrc 或平台自己的设置。
“至少需要”和“请用这个”,要分别写在两个地方。
查看选项与答案
- A. 不会,
engines只是声明,要另放.node-version(正确)——声明底线和指定版本是两件事:engines管底线,.node-version告诉平台实际用哪个,最好都写。没有它,平台就用自己的默认版本。 - B. 不会替你切换版本,但版本低于要求时 npm 会报错并中止安装,让构建直接失败——版本不够时,npm 只发一条警告,不会中止安装,更不会替你换版本。这次日志里的
EBADENGINE就只是警告,构建照样往下走。 - C. 会,平台读到
engines后,就会选用满足它的 Node 版本来构建——engines只是声明,npm 据此发警告;平台选版本,多半看.node-version、.nvmrc或平台自己的设置。
“至少需要”和“请用这个”,要分别写在两个地方。
3. 为了修一个构建错误,AI 建议“删掉 package-lock.json,重新 npm install”。风险是?
对了。重新生成时,所有依赖都会按版本范围取当前最新,等于一次性升级了几百个包。出了新问题,很难说清是哪个包、哪一次升级带来的。
你可能是这么想的:锁文件正是用来固定版本的。删掉之后,每个依赖都按版本范围重新解析,装出来的不一定和原来一样。
你可能是这么想的:新锁文件提交上去,升级也就一起提交了:几百个包在一次修错的提交里悄悄换了版本,出了新问题很难说清是哪一个。
锁文件是账本,别为了一个错误把整本账撕了重记。
查看选项与答案
- A. 所有依赖都按版本范围重新解析,等于一次性悄悄升级了几百个包(正确)——重新生成时,所有依赖都会按版本范围取当前最新,等于一次性升级了几百个包。出了新问题,很难说清是哪个包、哪一次升级带来的。
- B. 锁文件本来就是自动生成的,删了重来,结果和原来一样——锁文件正是用来固定版本的。删掉之后,每个依赖都按版本范围重新解析,装出来的不一定和原来一样。
- C. 只要把新生成的锁文件一起提交上去,就不会有别的风险——新锁文件提交上去,升级也就一起提交了:几百个包在一次修错的提交里悄悄换了版本,出了新问题很难说清是哪一个。
锁文件是账本,别为了一个错误把整本账撕了重记。
判断时刻
线上构建失败,报锁文件缺两个条目;本地一切正常。AI 给了三个方案。
你会选哪一个?
考察:让检查闭嘴
构建大概率会通过,因为 npm install 会自己补全锁文件。代价是线上每次构建都可能装出不同的版本,锁文件形同虚设。你关掉的是报警器,不是火。
考察:顺手升级
也能通过,但会按版本范围把所有依赖都升到当前最新,等于在一次修错的提交里,悄悄升级了几百个包。出了新问题,很难说清是哪一个。
考察:对齐环境
多花几分钟,只补上了锁文件里本该有的 8 个条目,没有升级任何依赖;再在 README 里写下“用 npm 10 生成锁文件”。这次选的就是这条。
构建失败时,先让本地和线上用同一套工具复现,再动手修。让检查变宽松、或者顺手升级一切,都会把今天的一个问题,换成以后的一堆问题。
三个选项各自的代价
- A. 把线上的安装命令从 npm ci 改成 npm install——考察让检查闭嘴:构建大概率会通过,因为 npm install 会自己补全锁文件。代价是线上每次构建都可能装出不同的版本,锁文件形同虚设。你关掉的是报警器,不是火。
- B. 删掉锁文件,重新生成一份——考察顺手升级:也能通过,但会按版本范围把所有依赖都升到当前最新,等于在一次修错的提交里,悄悄升级了几百个包。出了新问题,很难说清是哪一个。
- C. 用和线上相同的 npm 10 重新生成锁文件,在干净副本里模拟一遍 npm ci 和构建——考察对齐环境:多花几分钟,只补上了锁文件里本该有的 8 个条目,没有升级任何依赖;再在 README 里写下“用 npm 10 生成锁文件”。这次选的就是这条。
构建失败时,先让本地和线上用同一套工具复现,再动手修。让检查变宽松、或者顺手升级一切,都会把今天的一个问题,换成以后的一堆问题。
带走
下次让 AI 做这件事时,问它
- 线上构建用的是哪个版本的 Node 和包管理器、执行的是什么安装命令?和我本地有哪些不同?
- 这次新增或升级了哪些依赖?锁文件有没有一起更新并提交?
- 项目需要的最低 Node 版本是多少?有没有写进 .node-version 和 package.json 的 engines?
- 构建失败时,请先用和线上相同的版本在干净的副本里复现,再动手修。
自己验证
- 读构建日志时先看开头:构建机用的是哪个版本的 npm 和 Node。
npm ls 包名:看一个包是被谁带进来的、装的是哪个版本。- 推送前在一份干净的副本里跑一遍
npm ci和构建,模拟线上的安装方式。
本地能跑,只说明你的电脑能跑。让线上和本地用同一份锁文件、同一个版本,再下结论。
