本文目录
店铺关门后,值班人员仍然可以接待熟客、从备用仓库拿出菜单,但他不能假装后厨已经更新了今天的新菜。Service Worker(服务工作线程)就是页面之外的一位浏览器值班员:它可以拦截请求、读取缓存、等待页面再次打开,却不能直接操作页面 DOM。
一次请求经过谁的手
Service Worker 是否参与请求,取决于页面是否在它的控制范围内、Worker 是否已经激活,以及代码如何实现 fetch 事件。注册成功不等于当前页面立刻由它控制,首次打开常常要等下一次导航或主动调用 clientsClaim()。
注册、安装、激活不是一个事件
navigator.serviceWorker.register('/sw.js')
.then((registration) => registration.update())
// sw.js
self.addEventListener('install', (event) => {
event.waitUntil(caches.open('app-v1'))
})
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim())
})安装阶段可以准备资源,激活阶段适合清理旧缓存。浏览器会保留旧 Worker 服务已有页面,新版本可能处于 waiting 状态,直到旧页面全部关闭。强行跳过等待可以更快更新,却可能让同一页面同时混用旧 HTML 与新 JS。
Cache Storage 不是 HTTP 缓存
Cache Storage 是 Service Worker 可以主动读写的脚本缓存。它和浏览器根据 Cache-Control 自动管理的 HTTP 缓存不是同一个柜子。你需要决定缓存哪些 URL、何时失效、如何删除旧版本,也要处理缓存命中后数据已经过期的情况。
常见策略包括:
- cache first:优先离线缓存,适合版本固定的静态资源。
- network first:优先网络,失败时回退缓存,适合文章和接口数据。
- stale while revalidate:先给旧内容,同时后台请求新内容。
策略不是越复杂越好。HTML、API、图片和 JS 的更新风险不同,最好按资源类型分别设计。
PWA 不只是“加一个 manifest”
PWA(Progressive Web App,渐进式 Web 应用)通常还涉及 Web App Manifest、Service Worker、HTTPS、图标、安装体验和离线策略。Manifest 负责告诉浏览器应用叫什么、图标是什么、启动时打开哪里;Service Worker 负责运行时请求和缓存。两者是配合关系,不是同一个功能。
更新最容易出错的地方
缓存名称带版本号只能帮助识别旧缓存,不能自动清理。激活时应枚举 caches.keys(),删除不再使用的版本。还要避免把带用户数据的接口响应永久缓存,否则离线能力会变成展示旧信息的风险。
页面和 Worker 如何沟通
Service Worker 没有页面的响应式状态。页面可以通过 navigator.serviceWorker.controller.postMessage() 发送命令,Worker 可以用 clients.matchAll() 找到受控页面并回传消息。比如页面切换账号时通知 Worker 清理用户缓存,Worker 完成后再通知界面更新离线状态。
这种通信应该明确消息类型和版本,不能只发送一个没有约束的字符串。Worker 可能被终止后重新唤醒,内存中的变量也会丢失;需要长期保留的信息应放在 Cache Storage、IndexedDB 或服务器,而不是寄希望于 Worker 一直活着。
离线策略不是“缓存所有文件”
安装时预缓存应用壳可以让入口页面更快打开,但把大量图片、接口响应和第三方资源一股脑放进缓存,会增加首次安装时间和存储压力。运行时缓存更适合按访问情况逐步填充,并为不同资源设置上限、过期时间和清理规则。
离线页面还要处理表单提交、身份过期和数据冲突。离线时显示“已保存到本机”不等于“服务器已经收到”,用户重新联网后还需要同步队列、失败重试和明确的状态提示。PWA 的体验重点不是让页面看起来像原生应用,而是让网络不稳定时仍然诚实、可恢复。
开发时最容易误判的现象
Service Worker 的更新通常被浏览器缓存和生命周期影响。你改了 sw.js,旧 Worker 可能仍在控制当前页面;DevTools 的 Update on reload、Bypass for network 能帮助调试,但不能代表真实用户路径。上线前要从全新安装、旧版本升级、多个标签页和断网恢复四条路径分别验证。
如果应用使用预缓存资源,HTML、JS 和 CSS 的版本关系要一起考虑。旧 HTML 引用旧 JS 时,旧 Worker 可以继续服务;新 Worker 提前接管却可能拿到旧页面和新脚本的混合组合。一个稳妥的发布策略,是让资源文件带内容 hash,让 HTML 只承担入口和版本协商,并在升级失败时保留可回退的旧缓存。
离线功能还应提供可见的网络状态。浏览器的 navigator.onLine 只能说明网络连接线索,并不能证明接口可达;真正同步时仍要处理超时、服务器拒绝和重复提交。把缓存命中、网络成功、同步失败和用户主动刷新分别展示,用户才不会误以为一份本地旧数据已经完成云端保存。
Service Worker 的最佳使用场景,是把网络不确定性变成明确的产品状态:能离线读什么、何时显示旧数据、何时要求联网、更新失败如何恢复。技术方案越贴近这些问题,缓存规则就越容易维护。
缓存的目标不是永远返回旧内容,而是在可接受的条件下减少等待,并让失败有清晰出口。发布前至少走通:全新安装、旧版本升级、多标签页、断网恢复四条路径。
容易踩的坑
注册 Service Worker 不等于已经离线可用:必须有明确的缓存键与失败回退。清浏览器“缓存”也不等于清掉 SW 状态,Application 面板里要分别看 Service Workers、Cache Storage 和站点数据。skipWaiting 越早越好同样危险——新旧资源不兼容时,强行激活会让半页旧脚本配半页新资源。