跳转到内容

更新策略

发布于

目标: 让新版本 service worker 触达用户的方式是一个经过权衡的选择,而不是意外落地的 默认行为。具体机制(skipWaiting()、clients.claim()、waiting 状态)见 更新流程参考;本指南讨论的是该在 这几种策略中如何取舍。

如果不调用 skipWaiting(),新安装的 worker 会停留在 waiting 状态,直到所有由旧 worker 控制的标签页都关闭。默认行为偏向一致性:一个没有 service worker 的页面不会在会话中途 突然被接管;一个在某个 worker 版本下加载的页面,也不会被交给缓存假设不同的另一个版本。 对大多数应用而言,这种安静的默认行为就是正确的选择——但仅仅刷新并不能保证更新生效: 刷新期间,即使只有一个标签页,新旧页面的 client 也可能短暂重叠,因此只有当所有由旧 worker 控制的标签页都真正关闭或已导航离开时,等待中的 worker 才会接管。

在 install 事件中调用 self.skipWaiting(),会让新 worker 在进入 waiting 阶段后立即 踢掉当前活动的 worker 并自行激活,即便它仍在控制着已打开的标签页。直接在 install 处理函数中调用是常见做法:

self.addEventListener('install', (event) => {
self.skipWaiting();
});

代价是:这样一来,你的新 service worker 很可能正在控制着用旧版本加载的页面——如果新 worker 缓存的资源与页面上已有的 HTML 不兼容,就可能出问题。指导原则很直接——如果这可能 带来破坏,就不要无条件使用 skipWaiting()。

clients.claim() 是一个相关但独立的决定:它让新激活的 worker 接管那些在其注册时就已经 打开的页面。它只在首次加载时才真正有意义,因为得益于渐进增强,页面在没有 service worker 时通常也能正常工作——所以应把它当作按情况使用的选项,而非常规样板代码。

一种折中方案是把 skipWaiting() 推迟到由用户驱动的时刻,而不是在 install 中无条件 调用:新 worker 安装并等待,页面向用户展示“有可用更新”的提示,只有在用户操作之后,页面 才会通过 postMessage() 通知等待中的 worker 触发 skipWaiting(),随后进行一次可控的 刷新。

// 在页面中:监听 controller 变化,并在用户同意更新之后只刷新一次。
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) return;
refreshing = true;
window.location.reload();
});

这种模式——完整说明见更新流程参考—— 既避免了无条件 skipWaiting() 带来的隐性不一致风险,也避免了纯默认行为「更新要等所有 标签页关闭才生效」的代价。

浏览器会在导航到域内页面时自动检查是否有新的 worker 脚本,也会在诸如 push 与 sync 之类的功能性事件发生时检查(除非此前 24 小时内已经检查过)。如果预计用户会长时间保持 某个标签页打开、既不导航也不刷新,你可能需要按一定间隔(例如每小时)主动调用 registration.update(),让长时间存活的会话也能在合理时间内发现新版本:

navigator.serviceWorker.register('/sw.js').then((reg) => {
setInterval(() => reg.update(), 60 * 60 * 1000);
});
场景 建议策略
常规应用;用户最终会关闭或导航离开所有标签页 默认行为(不调用 skipWaiting())——最简单、最一致。
用户会长时间保持标签页打开(数小时/数天) 默认行为 + 周期性 registration.update() 检查,即使没有新的导航也能发现更新。
更新带有破坏性的缓存/schema 变化 默认行为,或用户确认后再刷新——绝不无条件使用 skipWaiting(),因为它可能接管带有不兼容假设的旧页面。
非破坏性修复,希望尽快生效,且可以接受一定干扰 在 install 中无条件调用 skipWaiting()。

← 返回指南总览。