Service Worker 发布激活:客户端兼容握手、缓存代际与安全回滚
新 Service Worker 安装成功不等于可以立刻接管旧页面,skipWaiting 还可能让旧客户端读到新协议缓存。本文用闭合发布清单、客户端握手和分代清理控制激活窗口。
本文要点
- 主题聚焦:TypeScript、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
PWA 渐进式 Web 应用介绍 Service Worker、Manifest 与离线缓存,Web 分层缓存策略解释 Cache First 和 Network First。生产事故却常发生在版本交界:页面 JS 仍是 release 17,新 worker 18 调用 skipWaiting() 后开始解释 release 18 的请求,或 activate 时删掉旧 cache,让尚未重载的页面离线白屏。本篇只治理 worker 发布与 cache 代际,不重新讲 PWA 入门。
一份清单闭合页面、Worker 和资产
构建阶段发布不可变 serviceWorkerRelease.v1,固定 worker script digest、页面 bundle/protocol 版本、precache manifest root、runtime cache schema、API 最低版本、允许接管的 client protocol 范围和回滚 reader。资产 URL 按内容摘要或 release ID 不可变;/latest.js 配长缓存再覆盖会让浏览器拿到混合制品。
schema: serviceWorkerRelease.v1
release: web-2026.07.19.18
workerDigest: sha256:41a...
clientProtocol: sw-client@5
compatibleClients: [4, 5]
precacheRoot: sha256:98c...
cacheSchema: runtime-cache@3
activationPolicy: DRAIN_OR_COMPATIBLE_CLAIM
install 只写 precache:<releaseDigest>:staging。每个响应必须是预期状态、类型、长度和摘要;重定向后的最终 URL 也要在清单内。全部对象验证后才写 completion marker,让 event.waitUntil() 决定 install 成败。Cache Storage 没有跨多个 put() 的全局事务,所以 marker 只能证明该不可变 namespace 已完整,不能把“cache 名存在”当 ready。浏览器 Wasm 资产完整性对多文件摘要的约束同样适用于 App Shell。
release 状态为 DISCOVERED -> INSTALLING -> STAGED -> WAITING -> ACTIVATING -> ACTIVE -> DRAINING -> RETIRED。浏览器的 installing/waiting/active worker 是生命周期事实源,应用 receipt 记录 worker digest、完整 cache root、激活策略和观察到的 client 集合,不能自行伪造另一个 active 指针。
激活前先问旧页面能不能共存
每个页面在加载后向 registration 报告 clientInstanceId、页面 release、client protocol、未提交操作数和是否可安全 reload。waiting worker 若参与盘点,必须用 clients.matchAll({ type: "window", includeUncontrolled: true }),再按 registration scope/storage key 过滤,并让回执绑定 inventory nonce、观察到的 client ID 集与逐项 ACK;默认 includeUncontrolled: false 会让尚未控制旧页面的 waiting worker 得到空或不完整集合。
枚举不是原子快照,新 client 可能在握手后加入。因此只有 release 声明兼容全部仍受支持的 client protocol 时,盘点才可辅助调用 skipWaiting();存在不兼容代际就保持默认 waiting,直到旧 client 排空。安全修复不能无限等待时,服务端先 fence 旧页面的高风险动作并要求重载。任何盘点遗漏仍由逐次 fetch/message 的协议检查拒绝,不能让一次“全集 ACK”成为永久通行证。
skipWaiting() 只改变等待策略,不是兼容证明;clients.claim() 会让当前页面发生 controller 变化,只有清单声明兼容且页面处理 controllerchange 时才允许。fetch/message 接受点同时校验实际 controller release、client protocol 与 request schema。旧页面发来的未知消息返回 CLIENT_RELOAD_REQUIRED,不能按新版默认字段继续。
cache 清理由 active worker 按 namespace 和 client receipt 执行。先停止向旧代写入,等已知旧 client 排空,再删除不被当前与 rollback release 引用的 cache;不能在 activate 一上来执行 caches.keys().map(delete)。运行缓存的响应还要绑定 schema 和 release,避免新 worker 把旧 JSON 当新版。浏览器存储配额恢复负责 origin 级压力,本协议只决定哪些 cache 仍有发布引用。
用混合版本故障验收
测试同时保持三个旧页面,注入 install 中断、单个资产 404、CDN 返回旧字节、completion marker 前崩溃、waiting worker 重启、离线激活、skipWaiting 与未提交表单竞争、clients.claim 后迟到旧消息,以及新 worker 删除旧 cache 后旧页发请求。断言失败 install 不影响当前 active;任何 fetch/message 都只落入兼容组合;未完成 cache 被采用为 0;已声明不可 reload 的页面丢状态为 0。
发布门要求 precache 摘要完整率 100%、旧/新协议误解码为 0、controller 变化后的错误请求为 0、离线黄金路径成功率 100%、激活后白屏率不高于基线且 cache 未引用字节在七天内收敛到预算。先仅安装到 waiting 并观测握手,再对内部用户启用兼容 claim,最后扩大。回滚不能让已 redundant 的旧 worker“复活”;应发布一个新 rollback release,复用上个已验实现和仍保留的资产,但拥有新的 worker digest 与单调发布序号。Feature Toggle可以停止新功能,版本握手与 cache 完整性仍不可关闭。
延伸阅读
Related