← 返回博客
Illustration of a PWA deployment pipeline with a service worker waiting to take over

PWA 部署指南:服务端条件、Service Worker 更新,以及生产环境里会坏的地方

大多数 PWA 教程写到 manifest 就结束了。而真正有意思的故障都发生在部署这一步:一个本地全绿、上线也顺利的应用,因为一个缓存响应头设错了,给回访用户连着三周发的都是旧壳。

部署 PWA=一次普通的网页部署,外加一份额外契约——浏览器必须能在你的生产源站上验证「可安装」,而你的 Service Worker 必须能在它已经接管之后,把新代码交到用户手里。本文讲清楚:服务端硬性条件、Service Worker 在生产环境里真实的生命周期行为、更新模型,以及那些反复出现的部署故障。

如果你还在确认字段本身对不对,逐字段的参考在PWA manifest 必填字段。本文默认 manifest 已经正确,只问一件事:推上线之后会发生什么。

服务端的硬性条件没有商量余地

有四件事必须在生产源站上成立,而不只是在你本地构建里成立:

HTTPS,而且是整条路径上都要。 注册 Service Worker 要求安全上下文。localhost 是被豁免的——这恰恰解释了为什么一个 PWA 在开发环境里完美工作、到生产环境却什么都没注册。混合内容也算:安装路径上只要有一个资源走了明文 HTTP 就可能足够。

manifest 可达,且以 JSON 类型返回。 内容类型返回错了,或者挡在一个鉴权跳转后面,都会静默失败。安装条件根本不成立,而页面上不会出现任何报错。

Service Worker 文件必须从它要控制的作用域下提供。 从子目录提供的 worker 无法控制它上层的路径。放在源站根目录是能规避这一整类问题的默认做法。

start_url 必须不经跳转直接解析。 如果它会跳转——地区分流、尾斜杠归一化、登录态回跳——安装后的应用可能打开到你没预期的位置,可安装性检查也可能直接不通过。

Service Worker 的生命周期就是你的部署模型

这是与你做过的其他部署最不一样的部分:Service Worker 不会在你上传新文件的那一刻替换自己。

真实顺序是:浏览器拉取新的 worker 文件,与已安装的那份逐字节比对,不同则安装新的。新 worker 随后进入等待状态。只要还有一个受它作用域控制的页面开着,它就不会激活;只有当作用域下所有标签页都关闭之后才接管——而对于一个用户挂在桌面上的 PWA,这个「之后」可以非常久。

由此引出三个后果,第一次生产部署的人全都会被它们绊到:

  1. 部署流水线变绿的时候,你的新代码并没有上线。 它是「已安装、在等待」。看板显示已发布,用户跑的还是旧壳。
  2. 强制刷新解决不了。 重新加载一个被控制的页面,控制者还是原来那个,旧 worker 依然在位。
  3. skipWaiting 不是免费的解法。 调用它会让新 worker 立即激活,但当前打开的这个页面,是用旧 worker 的资源加载出来的。新 worker 配旧页面的分包预期,正是生产环境里白屏和懒加载失败的来源。要用它,就得同时配一个提示或一次受控刷新,让页面和 worker 一起换。

诚实的选项只有两个:检测到有新 worker 在等待时提示用户刷新;或者接受自然交接,并把代码设计成新旧可以共存一段时间。每次部署都默默调一次 skipWaiting,是看起来最干净、实际最容易炸的那个选项。

缓存响应头是最贵的一个细节

避免最坏结果的那条规则是:Service Worker 文件本身,绝不能被 HTTP 层长时间缓存。

如果你的 CDN 或源站给 worker 配了很长的 max-age,那么在这段时间里浏览器根本不会去取新副本。取不到就没法做字节比对,就永远不知道有更新——于是你部署的任何东西都到不了已安装的用户那里,直到 TTL 过期为止。几乎所有「我们的 PWA 卡在旧版本上」的报障,机制都在这里。

给 worker 配 no-cache 或者极短的 max-age,把判断交给字节比对。带哈希的静态资源可以也应该激进缓存;worker 文件是唯一的例外,因为它是那个「必须变化,才能让其他一切变化」的东西。

同样的逻辑适用于 manifest,以及做了壳缓存的应用里的 index.html——它们是告诉浏览器其余一切长什么样的入口。

选缓存策略,别把自己堵死

缓存策略是按资源决定的,不是整个应用一个策略:

  • 带哈希的构建产物——cache-first 是对的。内容变了文件名就变,不可能过期。
  • 应用壳 / HTML 入口——network-first 加缓存兜底,或者 stale-while-revalidate。在这里用 cache-first,就是把用户困在旧版本上的那个动作。
  • 接口响应——凡是必须最新的走 network-first;只有在「显示稍旧的数据好过显示加载圈」的地方才用 stale-while-revalidate。
  • 用户维度的数据——要小心。缓存键里不带用户身份,会在共用设备上把上一个账号的数据发给下一个人。

给缓存名字带上版本,并在 worker 的 activate 阶段删掉旧版本。跨多次部署累积的缓存最终会撞上存储配额,而配额驱逐发生的时机不是你能控制的。

在生产源站上验收,不要在 localhost 上

「本地能跑」和「生产可安装」之间的落差,是最浪费时间的地方。把一次部署判为完成之前:

  • 用一个没有任何既有注册的全新浏览器配置打开线上源站,确认 worker 注册成功并进入 activated。
  • 确认安装入口出现——可安装条件是针对生产环境评估的,localhost 的豁免在那里不适用。
  • 断网重新加载。如果显示的是浏览器的离线页,说明 worker 注册了但没有在提供壳。
  • 确认 start_url 直接返回 200,中间没有跳转。
  • 部署一个肉眼可见的小改动,确认它能到达一个已经安装的实例。这是唯一真正跑通你更新路径的测试,也是最常被跳过的一个。

最后这一条值得说白:如果你从来没有验证过「第二次部署能到达一个已存在的安装」,那你就还不知道自己的更新机制是否可用——你只知道第一次部署成功了。

反复出现的部署故障

作用域不匹配。 worker 从构建输出目录提供,于是它静默地只控制那一棵子树。它上层的页面不受控制,结果应用一半能离线一半不能。

注册代码从来没执行。 注册调用写在一个更早就失败的脚本里,或者藏在大多数用户走不到的路由后面。不报任何错,只是对那部分用户来说,这个应用根本不是 PWA。

图标尺寸不满足要求。 可安装性判定不通过,通常除了「没有安装入口」之外没有任何可见症状。

换域名之后旧 worker 还在。 注册是按源站隔离的。换域名意味着每一个已安装用户都还留在旧源站、跑着旧 worker,而且不会自动跟过来。

把跳转的响应缓存了下来。 存下一个重定向响应并在之后重放,会产生在开发机上极难复现的导航行为。

没有回滚方案。 只把服务端回滚是不够的——必须让上一个 worker 成为字节比对里胜出的那份,也就是说你的回滚必须产出一个与坏版本不同的 worker 文件。重新部署一份更早的产物正好能做到;而指望单靠 CDN 刷新就能解决,不能。

它在分发版图里的位置

部署本身也是团队一开始选择这条路的原因:从构建完成到用户拿到,中间没有提交步骤、没有审核队列——你部署,更新按上面讲的生命周期扩散。这个性质才是它与商店分发真正的运营差异,比任何能力对比都更实在;完整取舍见 PWA 与 APK 的差别,安装流程的转化侧见 PWA 落地页怎么搭

ROIBest 面向的是这样一类团队:想要在生产环境里拿到这条安装路径,但不想自己从头搭一套服务端、安装提示与更新机制。

常见问题

部署完了,PWA 为什么还显示旧版本?

几乎总是两个原因之一:新 Service Worker 已安装但在等待,因为还有旧标签页控制着作用域;或者 worker 文件本身被配了很长的缓存时间,浏览器根本没取到新字节。先去查 worker 文件的 HTTP 缓存响应头。

一定要有 Service Worker 才算可安装吗?

可安装性是按 manifest 和安全上下文来判定的。Service Worker 提供的是离线能力和更新机制,所以即使在没有它的情况下出现了安装提示,这样的部署也称不上是有意义的 PWA。

Service Worker 文件应该放在哪?

除非有特定理由,否则放在源站根目录。它的位置决定它的作用域,子目录里的 worker 无法控制上层页面。

每次部署都调 skipWaiting 安全吗?

它是部署后白屏最常见的原因。当前打开的页面是基于旧 worker 的资源构建的。要用它,就在同一时刻触发一次页面刷新,让文档和 worker 一起换。

一次糟糕的 PWA 部署怎么回滚?

重新部署上一个产物,让 worker 文件的字节与坏版本不同,从而触发一次新的安装。只刷 CDN 并不能保证已安装的客户端重新评估,也解决不了一个已经激活的 worker。

PWA 能放在静态托管上吗?

能。要求是 HTTPS、正确的内容类型,以及对 worker 文件缓存响应头的控制权。任何能让你控制响应头的静态托管都够用。

一句话版本

走 HTTPS,把 worker 放在根目录,确认 start_url 不跳转,并且永远不要让 HTTP 层长期缓存 worker 文件。要明白部署变绿的含义是「已安装、在等待」,不是「已上线」。缓存策略按资源分别选,给缓存名带版本并在 activate 阶段清理。然后用一个全新浏览器配置在生产源站上验收——而且要验证第二次部署能到达一个已存在的安装,因为只有这一项能证明你的更新路径真的可用。

ROIBest

用 ROIBest 打造下一阶段增长引擎

联系我们,获取稳定、高转化的安卓 PWA 解决方案。