跳至主要内容
返回文章列表
约 6 分钟阅读

一次 Cloudflare Pages 构建失败排查:从 npm ci 锁文件报错到手机样式恢复

记录一次 QQ、百度手机浏览器页面样式异常的排查过程:从 Tailwind CSS 兼容性、Cloudflare 构建日志,到 npm ci 与 package-lock.json 不同步的修复和线上验证。

博客目录 →

最近用手机百度浏览器打开个人网站时,页面像是完全没有加载样式:导航变成普通链接,布局和间距消失,图标也挤在一起。电脑浏览器看起来正常,QQ 浏览器里也出现了类似现象。

一开始很容易把它当成单纯的 CSS 兼容问题。但这次排查发现,问题实际上分成了两层:样式本身需要兼容旧浏览器;而修复后的代码又没有成功部署。 真正把线索串起来的,是 Cloudflare Pages 的构建日志。

一、页面为什么看起来像“没加载 CSS”

浏览器访问静态网站时,大致会依次获取 HTML、样式表和 JavaScript:

打开网址
  → 获取 HTML
  → HTML 引用带哈希的 CSS 文件
  → 浏览器解析 CSS 并绘制页面

这次网页本身能打开,HTML 和 CSS 请求也能返回成功,但手机内置浏览器没有正确呈现样式。继续检查生产环境正在使用的 CSS,发现它是 Tailwind CSS 4.3.2 生成的,并包含较新的 CSS Cascade Layers(级联层)语法 @layer。

一些较旧或内核更新较慢的浏览器,对现代 CSS 语法的支持不完整。如果关键样式被放在浏览器不认识的规则中,它可能无法按预期应用这些规则,页面就会退化成缺少布局和装饰的状态。文件请求成功,不等于浏览器成功理解了文件内容。

针对这个兼容性问题,网站代码已调整为 Tailwind CSS 3.4,并补上相应的 PostCSS、Autoprefixer 和浏览器目标配置。按理说,重新构建并部署后,手机上的页面就应该恢复正常。

二、真正阻止修复上线的是 npm ci

代码改完推送后,Cloudflare Pages 的控制台却显示最新构建失败。打开构建日志,发现它还没运行 Astro 的网站构建命令,就在安装依赖阶段停下了:

npm ci can only install packages when package.json and package-lock.json are in sync
Missing: @emnapi/runtime@1.11.3 from lock file
Missing: @emnapi/core@1.11.3 from lock file
Missing: @emnapi/wasi-threads@1.2.3 from lock file

Cloudflare 日志里执行的 npm clean-install 是 npm ci 的别名。它和开发时常用的 npm install 不一样:npm ci 面向自动化构建,会严格按照 package-lock.json 安装;如果清单和锁文件描述的依赖树对不上,它会直接报错退出,而不是顺手帮你改锁文件。它还会在安装前清理已有的 node_modules,所以不要在重要的工作目录里随意用它做试验。npm 官方文档:npm ci

这次缺少的是一组可选依赖树中的条目。它们与 WASI(WebAssembly System Interface)相关构建依赖及其 @emnapi 支持包有关。本机安装时没有暴露问题,但 Cloudflare 使用 Linux 环境和自己的 npm 版本做干净安装,锁文件里的缺项就被严格检查出来了。

结果就是:浏览器兼容性代码虽然已经改好,Cloudflare 却因为依赖安装失败,根本没有构建这版代码。 线上继续提供上一次部署的 Tailwind 4 CSS,因此手机上仍然看到旧问题。这里要区分两个故障:前者是页面兼容性问题,后者是部署流水线阻止兼容性修复上线。

三、怎样修复锁文件

排查依赖问题时,先看构建日志中的 Node.js 和 npm 版本,再用与 CI 接近的 npm 版本更新锁文件。本次 Cloudflare 日志显示使用 npm 10.9.2,因此用该版本按 Linux x64 环境重新解析可选依赖,并且只更新 package-lock.json:

npx --yes npm@10.9.2 install \
  --package-lock-only \
  --include=optional \
  --os=linux \
  --cpu=x64 \
  --registry=https://registry.npmjs.org

其中:

  • --package-lock-only:只更新锁文件,不重装整个 node_modules。
  • --include=optional:把可选依赖也纳入解析和锁定。
  • --os=linux --cpu=x64:按部署环境解析平台相关依赖,避免只按本机平台生成结果。
  • --registry:示例使用 npm 官方仓库;如果项目依赖私有仓库或团队镜像,应换成项目实际使用的源。

重新生成后,锁文件补上了日志要求的 @emnapi/runtime、@emnapi/core 和对应的 @emnapi/wasi-threads 条目。package.json 没有因此增加直接依赖:修复的是依赖树的锁定信息,而不是把这些底层包变成项目源码直接调用的库。

如果本机平台与线上不同,可以在一次性临时副本里用 CI 对应的 npm 版本执行干净安装的模拟检查:

npx --yes npm@10.9.2 ci \
  --dry-run \
  --ignore-scripts \
  --no-audit \
  --no-fund \
  --os=linux \
  --cpu=x64 \
  --registry=https://registry.npmjs.org

本次就在只包含 package.json 和 package-lock.json 的临时目录里运行了这项检查,结果通过。之所以不用正在开发的网站目录做 npm ci,是因为它会清理 node_modules;即使只是验证,也优先在临时副本或全新的 CI 环境里操作。npm install --package-lock-only 可以按锁文件模式解析并更新锁文件,相关选项见 npm 官方文档:npm install。

四、部署成功后,检查实际发布的 CSS

锁文件修复提交并推送后,Cloudflare Pages 重新构建成功。除了看控制台里的构建状态,还要确认线上页面真的切到了新产物:

  1. 打开首页 HTML,查看它引用的 CSS 文件名是否已更新。
  2. 请求新的 CSS 文件,确认返回状态正常。
  3. 检查生成的样式内容,确认它不再包含旧版构建中的 @layer 规则。
  4. 最后用目标手机浏览器重新打开页面;如果仍显示旧样式,再清理浏览器缓存或用无痕窗口复测。

本次线上首页已经换成新的哈希 CSS 资源,检查结果中不再包含 @layer。哈希文件名会随构建内容变化,因此更可靠的做法是比较“当前 HTML 引用的文件”和该文件的实际内容,而不是长期记住某个固定文件名。

Cloudflare Pages 根据构建命令的退出状态判断构建成功与否;构建失败时,不会把未完成的版本当作成功产物发布。项目使用 Astro 时,Pages 的常见构建命令是 npm run build,构建输出目录是 dist。Cloudflare Pages 官方文档:构建配置

五、遇到类似问题时,按这条顺序排查

现象先检查什么常见方向
页面像没加载样式HTML 是否引用 CSS、CSS 请求状态、CSS 中的语法资源路径错误、浏览器不支持某些 CSS 语法
本地已经修好,线上还是旧样子Pages 最新部署是否成功、当前 HTML 引用的资源哈希构建失败、部署未触发、浏览器缓存
npm ci 提示锁文件不同步报错中列出的缺失包、CI 的 npm 版本和系统平台更新锁文件、核对可选依赖和平台条件
本机安装成功但 Linux CI 失败本机与 CI 的 Node/npm、操作系统、CPU 架构在目标平台或干净容器中重现安装

这次最重要的经验是:“代码已经改了”不等于“线上已经运行新代码”。 浏览器兼容问题要检查最终 CSS;部署问题要检查构建日志和实际发布资源。沿着“源代码 → 依赖安装 → 网站构建 → 产物发布 → 浏览器解析”这条链路逐段确认,才能知道修复卡在了哪一环。

打开原图