当我们使用各种技巧以充分爆发
Service Worker的小宇宙时,往往因忽略其自身的更新问题,从而造成各种意想不到的故障。基于此,本章我们将目光重新回到Service Worker上,来聊一聊它的更新处理。
# Service Worker 脚本命名
现代前端构建体系中,输出的静态资源(脚本、图片、样式等)都以
[name].[hash].[ext]的格式命名(比如:index.54a427d9cf.js),这是因为此类文件内容变更的频率非常低,我们可以对其使用强制缓存来避免不必要的网络请求,并且在文件变更之后,能够在页面刷新时得到更新。那么这种成为业界标准的命名是否适用于 Service Worker 脚本呢?我们通过一个例子进行说明:
- 假设我们在
index.html中注册了某版本的 Service Worker(文件名为:sw.v1.js),并通过预缓存将index.html添加到缓存中。 - 当
Service Worker更新后,index.html需要注册新版本的 Service Worker(文件名为:sw.v2.js), - 如果我们使用缓存优先或仅缓存策略来响应
index.html请求,除非用户手动清除缓存,否则从缓存中得到的index.html中注册的Service Worker依旧为sw.v1.js,sw.v2.js将永久不会生效。
正是由于以上所述缘由,当我们处理 Service Worker 脚本时,一定要保证不同版本的文件名保持一致。
# Service Worker 脚本缓存
如果已存在一个版本的
Service Worker,那么再次触发其安装的条件是从服务器获取的sw.js与本地版本存在差异。如果我们对 sw.js 进行缓存,除非用户手动清除缓存或缓存失效,否则新版本的 Service Worker 将永远无法安装。因此,不要对 Service Worker 脚本设置缓存(服务端可通过max-age: 0响应头来避免缓存)
# 慎用 skipWaiting
Service Worker 安装成功后,如果已存在一个版本的 Service Worker 且有页面尚未关闭,新版 Service Worker 便会进入等待状态,直到运行旧版本的页面全部关闭或在 install 事件中调用 skipWaiting 方法,新的版本才会进入激活状态。虽然我们可以通过调用 skipWaiting 方法让新版本尽快接管页面控制权,但这种过早的权利交接很可能造成一些意想不到的问题,比如:
假设 Service Worker 对请求 /users/tom 的响应进行了转换,在旧版本中返回的格式为:
{
name: 'Tom',
city: 'NanJing'
}
新版本中返回的格式为:
{
v2Name: 'Tom',
v2City: 'NanJing'
}
这样的不一致可能在某些情况下导致应用崩溃且难以复现,这就等于在应用中埋藏了一个不稳定的定时炸弹,所以,除非能够保证同一个页面在两个版本相继处理的情况下依旧能够正常工作,否则尽量避免使用
skipWaiting方法
# 处理更新的正确姿势
上文说到滥用
skipWaiting可能会带来意想不到的问题,那如果我们想要新版本的Service Worker尽快接收控制权,又该如何处理呢?我的建议是将控制权交给用户,比如:

上图中,当新版本的
Service Worker安装成功后,给予用户提示,并让用户自行决定是否进行更新,如果用户确定更新,我们便激活新版本,并在激活成功后给予提示并刷新页面(如下图)。

更新提示显示时机代码实现:
export async function initSW() {
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.register('/sw.js');
//... 其他逻辑
if (registration.waiting) {
showSwUpdateTip(registration);
}
registration.addEventListener(