跳转到内容

Service worker 更新流程与 skipWaiting

一句话: 当浏览器发现字节不同的 service worker 脚本时,它会将新版本安装在旧版本旁边,并将其保持在 waiting 状态,直到所有受控标签页关闭——除非你调用 self.skipWaiting() 跳过这个等待。

浏览器会在每次导航(scope 范围内)以及显式调用 registration.update() 时自动重新获取 worker 脚本。若获取到的脚本与当前已注册的版本哪怕只有一个字节不同,浏览器就会开始新的安装流程。

更新检查有一条特殊的新鲜度规则:对于距上次检查超过 24 小时的 worker 脚本,即使服务器设置了激进的 Cache-Control 头,浏览器也会绕过 HTTP 缓存。注册时使用 updateViaCache: 'none' 选项可让每次检查都完全绕过缓存。

新 worker 成功完成安装(install 事件 resolve)后,进入 waiting 状态。在以下条件满足之前,它不会激活,也不会处理 fetch 事件:

  1. worker 控制的所有标签页全部关闭,
  2. 新 worker 调用 self.skipWaiting()

这一设计保证了页面与为其提供服务的 worker 始终来自同一版本。运行旧版 HTML 的标签页不会被交给可能使用不同缓存键或 API 的新 worker 接管。

install 事件中调用 skipWaiting() 会告知浏览器立即激活新 worker,即使旧版本仍在控制打开的标签页:

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('app-v2').then((cache) => cache.addAll(['/index.html', '/app.js']))
);
self.skipWaiting(); // 安装后立即激活
});

skipWaiting() 是异步的,但其效果相对于当前任务通常是即时的。在 install 外部调用也有效,但放在 waitUntil 内部是最可靠的做法。

风险: 若新 worker 的缓存资源与旧 worker 已加载的 HTML 不兼容,在不刷新所有标签页的情况下调用 skipWaiting() 可能引发运行时错误。务必搭配 controllerchange 监听器一起使用。

默认情况下,新激活的 worker 不会控制注册时已打开的页面——这些页面在当前生命周期内处于无 worker 控制状态。clients.claim() 让新 worker 立即接管这些页面:

self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== 'app-v2').map((k) => caches.delete(k)))
)
);
self.clients.claim(); // 接管 scope 内所有打开的标签页
});

只有在有明确理由时才使用 clients.claim()——例如,确保注册后首次加载即受控(使离线功能立即生效)。不必要地使用 claim() 会导致所有当前打开的标签页在会话中途更换 worker。

skipWaiting() 激活新 worker 后,现有标签页仍在运行旧代码。推荐的做法是在页面中监听 controllerchange 并提示用户刷新:

// 在页面中
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) return;
refreshing = true;
window.location.reload();
});

也可以使用 registration.waiting 属性检测待更新状态,展示“刷新以更新”的 UI,而非强制自动刷新。

调用 registration.update() 可立即检查是否有新版本 worker 脚本:

navigator.serviceWorker.ready.then((registration) => {
registration.update();
});

原生 Service Workers API 需要你自行调用 registration.update() 以实现主动更新检测。若使用 Workbox,其 workbox-window 包提供了 wb.addEventListener('waiting', ...) 用于响应更新事件,但定期重新检查仍需显式调用 wb.update() 或依赖浏览器导航时自动触发的检查。

  • 注册时使用 updateViaCache: 'none',确保浏览器始终重新获取 worker 脚本。
  • 若使用 skipWaiting(),同时在页面中添加 controllerchange 监听器以刷新标签页,避免版本不一致。
  • activate 事件中清理旧缓存名称,确保在 skipWaiting() 让新 worker 激活后及时清理。
  • 对关键应用,考虑展示可见的“有可用更新——点击刷新”提示,而非静默自动刷新。
  • 通过“加载页面→修改 worker→导航(非刷新)“流程测试更新行为,而非仅在 DevTools 中强制刷新。