Service Worker · 概念
Service worker 生命周期
发布于
service worker 在处理任何 fetch 事件之前,要按固定顺序经过 installing → installed → activating → activated 四个状态;新版本会在旧版本旁边安装,但停留在 installed(waiting)状态,直到被旧 worker 控制的标签页全部关闭。这套状态机在 Chrome 40、Firefox 44、Safari 11.1 与 Edge 17 中完全一致(BCD api.ServiceWorker.state),浏览器之间的差别只在于观察它的工具。
navigator.serviceWorker.register(scriptURL) 抓取并解析脚本。解析成功后,浏览器创建一个状态为 installing 的 ServiceWorker 对象,在 ServiceWorkerRegistration 上触发 updatefound,并把该 worker 暴露为 registration.installing。之后每一次状态转换都会在这个 ServiceWorker 对象上触发 statechange,因此页面用一个监听器就能跟完整个序列。
| 状态 | worker 内的事件 | 浏览器的动作 |
|---|---|---|
installing |
install |
运行处理函数;event.waitUntil(promise) 让状态停留到 Promise 落定。拒绝会使 worker 变为 redundant,旧版本原地保留。 |
installed |
无 | worker 成为 registration.waiting。若没有其他 worker 正在控制客户端,它立刻进入下一步;否则等到最后一个受控客户端关闭,或 worker 自己调用 self.skipWaiting()。 |
activating |
activate |
运行处理函数,同样由 waitUntil() 保持。旧缓存在这里删除,因为上一版 worker 已不可能再运行。 |
activated |
fetch、message、push、sync…… |
worker 成为 registration.active,控制 scope 内的新导航。已打开的页面继续沿用原控制者,除非 worker 调用 self.clients.claim(),这会在每个受影响页面的 navigator.serviceWorker 上触发 controllerchange。 |
redundant |
无 | 安装失败,或已被更新的 worker 取代。对象在页面释放之前仍可访问。 |
两条规则让这个序列对更新是安全的。等待规则保证页面与为它服务的 worker 来自同一次部署:仍在运行旧 HTML 的标签页不会被交给缓存名不同的 worker。空闲规则意味着 worker 不是常驻进程:事件之间浏览器会终止它(按 web.dev 生命周期文章所述,Chrome 在空闲约 30 秒后终止),下一个事件再重新启动,所以需要留存的数据都要放进 Cache Storage 或 IndexedDB,而不是全局变量。
开启新一轮周期的更新检查发生在每次进入 scope 的导航、registration.update() 调用以及 push 与 sync 事件时,它逐字节比较抓取到的脚本与已安装脚本。相差一个字节就会启动新的 installing worker;完全相同则什么也不做。浏览器对该脚本只遵守最多 24 小时的 HTTP 缓存(注册选项 updateViaCache 控制主脚本与导入脚本是否允许来自 HTTP 缓存)。
第一个示例记录每一次状态转换,让这个序列可以直接在页面控制台里读到;第二个示例选定更新策略,并在 API 缺失时平稳降级。
在页面中记录每次状态转换
Section titled “在页面中记录每次状态转换”updatefound 对每个新 worker 触发一次;statechange 在该 worker 的每次转换时触发;controllerchange 在页面的控制者更换时触发。三者合起来就是上表在运行时的重现。
const registration = await navigator.serviceWorker.register('/sw.js');
registration.addEventListener('updatefound', () => { const worker = registration.installing; console.log('new worker:', worker.state); // "installing" worker.addEventListener('statechange', () => { console.log('state ->', worker.state); // installed、activating、activated | redundant });});
navigator.serviceWorker.addEventListener('controllerchange', () => { console.log('controller is now', navigator.serviceWorker.controller?.scriptURL);});首次访问时,除非 worker 调用了 clients.claim(),controllerchange 不会触发:页面加载时没有控制者,并一直保持到下一次导航。首次激活后刷新页面,navigator.serviceWorker.controller 从一开始就有值。
选定更新策略,并为没有该 API 的浏览器准备回退
Section titled “选定更新策略,并为没有该 API 的浏览器准备回退”默认的等待行为就是安全策略,不需要任何代码。下面的代码只在页面准备好刷新时才选择立即接管;当 serviceWorker 不存在(非安全源,或浏览器禁用了该 API)时,让应用停留在仅在线模式,而不是抛错。
if (!('serviceWorker' in navigator)) { document.documentElement.dataset.offline = 'unsupported'; // 仅在线 UI;不显示更新提示} else { navigator.serviceWorker.register('/sw.js').then((registration) => { registration.addEventListener('updatefound', () => { registration.installing.addEventListener('statechange', (e) => { if (e.target.state === 'installed' && navigator.serviceWorker.controller) { showReloadBanner(() => registration.waiting.postMessage('SKIP_WAITING')); } }); }); }); let reloading = false; navigator.serviceWorker.addEventListener('controllerchange', () => { if (!reloading) { reloading = true; location.reload(); } });}
// sw.jsself.addEventListener('message', (event) => { if (event.data === 'SKIP_WAITING') self.skipWaiting();});立即接管策略的代价是每次更新强制刷新一次,并且必须保留 controllerchange 守卫,否则第二次 controllerchange(例如来自 clients.claim())会让页面刷新两次。默认策略的代价是一直不关最后一个标签页的用户会停留在旧版本,直到关闭它为止。
- Service Workers specification: Service worker lifetime(w3c.github.io)
- The service worker lifecycle(web.dev)
- ServiceWorker: state property(developer.mozilla.org)
- skipWaiting() 与更新流程
- Service worker 注册与 scope
- Service worker 调试
- Clients API
规范
| 规范 | 状态 |
|---|---|
| Service Worker | W3C 草案 |