跳转到内容

Service Worker · API

skipWaiting() 与更新流程

发布于

浏览器抓取到的 service worker 脚本与已安装版本哪怕只差一个字节,就会在旧 worker 旁边安装新 worker,并把它保持在 waiting 状态,直到旧 worker 控制的所有页面都已关闭;self.skipWaiting() 解除这个等待,clients.claim() 则让已激活的 worker 接管它尚未控制的页面。这一流程的四个部分(skipWaiting()、clients.claim()、registration.update() 与 updateViaCache 选项)自 Chrome 41、Firefox 44、Safari 11.1、Edge 17 起可用(BCD api.ServiceWorkerGlobalScope.skipWaiting),只有 updateViaCache 较晚,自 Chrome 68、Firefox 57、Safari 11.1 起可用。

Service worker 更新策略流程图:更新检查比较脚本字节,把新 worker 安装到 waiting 状态,随后三选一。A:等所有标签页关闭后再 activate;B:调用 self.skipWaiting() 与 clients.claim(),并在 controllerchange 时重新加载;C:监听 updatefound,显示“有可用更新”控件,发送 SKIP_WAITING 消息并在 controllerchange 时重新加载。
处理 waiting 状态 service worker 的三种方式:等待(默认)、立即接管,或询问用户。
// service worker 中
self.skipWaiting(); // Promise<undefined>
self.clients.claim(); // Promise<undefined>
// 页面中
const registration = await navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' });
await registration.update(); // 以该注册兑现
registration.waiting; // 没有等待中的 worker 时为 null
registration.addEventListener('updatefound', handler);
navigator.serviceWorker.addEventListener('controllerchange', handler);

浏览器会在 scope 内发生导航、push 或 sync 事件触发,以及功能性事件距上次检查超过 24 小时到达时自动运行更新检查;registration.update() 按需触发同一检查。install 处理器被拒绝的 worker 不会进入 waiting,而是直接变为 redundant。

四个成员中两个不接受参数,另外两个分别接受一个注册选项和零个参数。

成员 位置 参数 作用
self.skipWaiting() worker 无 标记该 worker 在当前 active worker 正在运行的事件结束后立即激活,而不是等它的所有客户端关闭。通常在 install 中调用。
self.clients.claim() worker 无 让这个 active worker 成为 scope 内所有无控制器或由更旧 worker 控制的客户端的控制器;在这些页面中触发 controllerchange。只有 worker 处于 active 状态时才有效,因此应放在 activate 中。
registration.update() 页面 无 重新抓取脚本(遵守 updateViaCache)并逐字节比较;无论是否发现新 worker 都以该注册兑现。
updateViaCache register() 选项 'imports'(默认)、'all' 或 'none' 更新检查期间允许 HTTP 缓存提供哪些脚本:只有 importScripts() 的依赖、全部,或都不允许。与此选项无关,浏览器一律忽略超过 24 小时的缓存副本。

skipWaiting() 从不拒绝:规范规定它在 worker 激活后、或已处于 active 状态时以 undefined 兑现。其他成员可能拒绝。

  • clients.claim() 在该 worker 不是注册的 active worker 时以 InvalidStateError 拒绝,例如在脚本顶层调用而 worker 仍在安装中。
  • registration.update() 在抓取的脚本加载失败或 MIME 类型不是 JavaScript 类型时以 TypeError 拒绝,在注册已被注销或其最新 worker 为 null 时以 InvalidStateError 拒绝。
  • register() 在 updateViaCache 不是三个允许值之一时以 TypeError 拒绝。

调用 skipWaiting() 却不重新加载受控页面,会让这些页面继续运行上一次部署的 HTML,而 worker 的预缓存里已是新部署的内容;页面稍后懒加载某个已不存在的带哈希分块时会得到 404。示例中的 controllerchange 重新加载正是为了关上这个窗口。

三个示例分别对应图中的三条路径:立即接管、询问用户后更新、按需检查。

用 skipWaiting()、claim() 与带保护的重新加载立即接管

Section titled “用 skipWaiting()、claim() 与带保护的重新加载立即接管”

worker 在 install 中跳过等待、在 activate 中接管客户端;页面在 controllerchange 时重新加载一次。refreshing 标志防止 claim() 第二次触发该事件时陷入重载循环。

sw.js
self.addEventListener('install', (event) => {
event.waitUntil(caches.open('app-v2').then((cache) => cache.addAll(['/', '/app.js'])));
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});
// page.js
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) return;
refreshing = true;
location.reload();
});

这条路径的代价是每个打开的标签页都会在会话中途重新加载,包括用户正在填写表单的那个;它适合页面生命周期短的应用。

页面监听 updatefound,等新 worker 到达 installed 后显示控件;用户点击后才向 worker 发消息,由 worker 调用 skipWaiting()。

page.js
const registration = await navigator.serviceWorker.register('/sw.js');
registration.addEventListener('updatefound', () => {
const worker = registration.installing;
worker.addEventListener('statechange', () => {
if (worker.state === 'installed' && navigator.serviceWorker.controller) {
showUpdateButton(() => worker.postMessage({ type: 'SKIP_WAITING' }));
}
});
});
navigator.serviceWorker.addEventListener('controllerchange', () => location.reload());
// sw.js
self.addEventListener('message', (event) => {
if (event.data?.type === 'SKIP_WAITING') self.skipWaiting();
});

navigator.serviceWorker.controller 的检查跳过了首次安装,因为那时没有旧版本可替换。如果页面同时在第二个标签页中打开,那个标签页也会在 controllerchange 时重新加载。

一个从不导航的长寿命单页应用,否则只会每 24 小时检查一次更新。下面的代码每小时检查一次,并在 navigator.serviceWorker 缺失时降级为什么都不做;在所有实现该 API 的浏览器中,非安全源(非 localhost 的纯 HTTP)都是这种情况。

if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then((registration) => {
setInterval(() => registration.update().catch(() => {}), 60 * 60 * 1000);
});
} else {
// 没有 service worker:页面总是运行已部署的版本,无需检查更新。
}

离线时 update() 被拒绝是预期行为;在这里吞掉它是正确的,因为下一个周期会重试。

规范

规范状态
无。