K 的一隅

浏览器 浏览器核心机制

页面关掉以后谁还在工作:Service Worker 与 PWA

从店铺值班和备用仓库说起,理解 Service Worker、请求拦截、Cache Storage 与离线更新。

5 分钟阅读 更新于 2026-07-24
本文目录

店铺关门后,值班人员仍然可以接待熟客、从备用仓库拿出菜单,但他不能假装后厨已经更新了今天的新菜。Service Worker(服务工作线程)就是页面之外的一位浏览器值班员:它可以拦截请求、读取缓存、等待页面再次打开,却不能直接操作页面 DOM。

一次请求经过谁的手

Service Worker 是否参与请求,取决于页面是否在它的控制范围内、Worker 是否已经激活,以及代码如何实现 fetch 事件。注册成功不等于当前页面立刻由它控制,首次打开常常要等下一次导航或主动调用 clientsClaim()

注册、安装、激活不是一个事件

js
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 越早越好同样危险——新旧资源不兼容时,强行激活会让半页旧脚本配半页新资源。