跳转到内容

更新策略

发布于

完成本指南后,新版本的 Service Worker 会按你选定的方式触达用户,并且有实现该选择的页面代码与 worker 代码: 浏览器的等待默认行为、立即接管,或由用户从「有可用更新」提示中触发的刷新。机制本身(skipWaiting()、 clients.claim()、waiting 状态)见 Service worker 更新流程与 skipWaiting; 本指南讲的是选哪一种以及如何接线。

你需要一个脚本在每次部署之间会变化的已注册 worker。抓取到的脚本与浏览器持有的逐字节不同时,浏览器就视为 有更新;检查发生在导航到 scope 内页面时、push 与 sync 事件时(除非前 24 小时内已检查过), 以及脚本 URL 改变后的 register() 时。多数浏览器在这个检查中忽略 HTTP 缓存头。

不调用 skipWaiting() 时,新安装的 worker 会等到旧 worker 控制的客户端数为零。默认行为换来的是一致性: 页面始终用它加载时的那个 worker 版本,于是它所依赖的缓存就是作答的缓存。代价是刷新不够:刷新期间新旧页面 短暂重叠,旧 worker 仍控制着一个客户端,所以只有用户关闭或离开所有标签页之后更新才生效。 如果用户会在一天内关闭标签页,并且这段时间内的旧版本可以接受,就选它。

// sw.js:什么都不用加。install 处理函数只做预缓存。
self.addEventListener('install', (event) => {
event.waitUntil(caches.open('shell-v4').then((cache) => cache.addAll(['/', '/app.js'])));
});

在 install 中调用 self.skipWaiting(),新 worker 在安装完成后立刻激活,哪怕旧 worker 的页面还开着。 当首次安装就应当控制那些没有 worker 时加载的页面,再在 activate 中配上 clients.claim()。 代价是:新 worker 开始为旧版本生成的 HTML 作答,所以一个不向后兼容的缓存或 schema 变更会弄坏这些已打开的页面。 用它来发布应当立即生效、且不具破坏性的修复。

sw.js
self.addEventListener('install', (event) => {
self.skipWaiting(); // 它返回的 Promise 可以忽略
event.waitUntil(caches.open('shell-v4').then((cache) => cache.addAll(['/', '/app.js'])));
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});

折中方案是安装新 worker、让它等待、告知用户,只在用户接受时才调用 skipWaiting(),随后做一次受控的刷新。 它既避免了无条件 skipWaiting() 的隐性不一致,也避免了默认行为「更新迟迟不生效」的代价。 worker 监听一条消息;页面观察注册对象是否出现等待中的 worker,并在 controllerchange 时只刷新一次。

sw.js
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting();
});
// 页面
const registration = await navigator.serviceWorker.register('/sw.js');
function offerUpdate(worker) {
const banner = document.querySelector('#update-banner');
banner.hidden = false;
banner.querySelector('button').onclick = () => worker.postMessage({ type: 'SKIP_WAITING' });
}
if (registration.waiting) offerUpdate(registration.waiting);
registration.addEventListener('updatefound', () => {
const installing = registration.installing;
installing.addEventListener('statechange', () => {
if (installing.state === 'installed' && navigator.serviceWorker.controller) offerUpdate(installing);
});
});
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) return;
refreshing = true;
location.reload();
});

navigator.serviceWorker.controller 这个检查让首次安装时不弹横幅,因为那时没有旧 worker 可替换。

在长期打开的标签页中检查更新

Section titled “在长期打开的标签页中检查更新”

一个开了几天的标签页从不导航,所以从不触发浏览器的检查。registration.update() 抓取脚本并在不同时安装, 上一次抓取距今超过 24 小时时会绕过 HTTP 缓存。每小时调用一次能让自助终端或仪表盘与最新部署的差距保持在 一小时之内,代价是每个标签页每小时多一个小请求。

navigator.serviceWorker.register('/sw.js').then((registration) => {
setInterval(() => registration.update(), 60 * 60 * 1000);
});
场景 策略 代价
用户每天都会关闭或离开标签页 等待的默认行为 最后一个标签页关闭后更新才生效
标签页会开上几小时甚至几天 默认行为加每小时一次 registration.update() 每个标签页每小时多一个请求
发布改变了缓存布局或 IndexedDB schema 默认行为,或用户确认后刷新 要设计一条横幅,用户必须接受一次刷新
应当立即生效的非破坏性修复 在 install 中调用 skipWaiting() 新 worker 为旧版本构建的页面作答

部署一个逐字节有变化的 sw.js,在两个标签页里打开应用,在 Service workers 面板里观察所选行为: 默认行为在两个标签页都关闭前一直显示 waiting to activate;skipWaiting() 在下一次刷新时显示新 worker activated and is running;确认式流程先出横幅,再刷新。

← 返回指南总览。