最近用手机百度浏览器打开个人网站时,页面像是完全没有加载样式:导航变成普通链接,布局和间距消失,图标也挤在一起。电脑浏览器看起来正常,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 重新构建成功。除了看控制台里的构建状态,还要确认线上页面真的切到了新产物:
- 打开首页 HTML,查看它引用的 CSS 文件名是否已更新。
- 请求新的 CSS 文件,确认返回状态正常。
- 检查生成的样式内容,确认它不再包含旧版构建中的
@layer规则。 - 最后用目标手机浏览器重新打开页面;如果仍显示旧样式,再清理浏览器缓存或用无痕窗口复测。
本次线上首页已经换成新的哈希 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;部署问题要检查构建日志和实际发布资源。沿着“源代码 → 依赖安装 → 网站构建 → 产物发布 → 浏览器解析”这条链路逐段确认,才能知道修复卡在了哪一环。