5月28日 02:21

Service Worker 的更新机制是怎样的?

Service Worker 更新机制核心回答

浏览器通过字节级对比检测 Service Worker 文件变化,发现差异后启动更新流程:新 Worker 安装 → 进入 waiting 状态 → 旧 Worker 控制的页面全部关闭后激活。整个过程是非破坏性的——旧版本继续服务已有页面,新版本等待接管。

三个关键点决定更新行为:

  1. 什么时候检查更新? 用户导航到 Service Worker 作用域内的页面时,或 functional event(push/sync)触发时(距上次检查超过 24 小时),以及手动调用 registration.update() 时。
  2. 为什么新版本不能立即生效? 默认策略保证页面生命周期内 Service Worker 不变,避免同一个页面被新旧两个 Worker 同时控制导致状态不一致。
  3. 如何让新版本提前生效? 在新 Worker 中调用 self.skipWaiting() + 在主线程监听 controllerchange 事件后刷新页面。

追问:skipWaiting 会有什么问题?

skipWaiting() 让新 Worker 立即激活,但已有页面可能正由旧 Worker 处理请求。新 Worker 的 fetch 事件处理逻辑可能与旧版本不同,导致同一页面中部分请求走新逻辑、部分走旧逻辑,出现不一致。实际项目中应该配合用户提示:检测到新版本 → 弹出提示 → 用户确认后调用 skipWaiting → 监听 controllerchange 刷新页面。

更新触发的具体时机

浏览器的更新检查并非"每次访问都检查",而是有明确的触发条件:

  • 导航事件:用户访问 Service Worker 作用域内的页面时,浏览器会请求 SW 文件并对比字节。如果服务器返回的文件与本地缓存的版本有字节差异,就触发更新。
  • Functional event:push、sync 等事件触发时也会检查更新,但有 24 小时的最小间隔,避免过于频繁的网络请求。
  • 手动触发:调用 registration.update() 强制检查,不受 24 小时限制。
  • register() 调用:只有 SW 文件的 URL 发生变化时才会触发更新检查。如果 URL 不变,register() 不会额外发起请求。

一个容易被忽略的细节:浏览器对 SW 文件的请求默认会加上 Cache-Control: no-cache 的语义,即使服务器返回了缓存头,浏览器也会尝试条件请求(If-Modified-Since / If-None-Match)。所以服务器必须正确配置 SW 文件的响应头

shell
Cache-Control: no-cache, no-store, must-revalidate

如果服务器把 SW 文件缓存了(比如 CDN 配置不当),浏览器拿到的永远是旧文件,更新永远不会触发。

追问:如何确认浏览器是否检测到了更新?

在 DevTools → Application → Service Workers 面板中可以看到当前状态。代码中监听 updatefound 事件可以捕获到新 Worker 的出现:

javascript
navigator.serviceWorker.register('/sw.js').then(reg => { reg.addEventListener('updatefound', () => { const newWorker = reg.installing; console.log('检测到新版本:', newWorker); }); });

更新生命周期详解

安装阶段

新 Worker 被发现后,浏览器执行其 install 事件。这一步通常用来预缓存新版本的资源:

javascript
const CACHE_NAME = 'app-v2'; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME).then(cache => cache.addAll(['/index.html', '/app.js', '/styles.css']) ) ); });

如果 install 事件回调执行失败(比如某个资源加载失败),新 Worker 会被直接丢弃,旧 Worker 继续工作。这就是更新过程的容错机制——安装失败的 Worker 不会影响线上服务。

等待阶段

安装成功后,新 Worker 进入 waiting 状态。它必须等到旧 Worker 控制的所有页面(clients)都关闭后才能激活。注意:刷新页面不算关闭。用户需要关闭所有标签页再重新打开,或者通过代码干预。

这是很多开发者困惑的地方:为什么更新了 SW 文件但页面行为没变?答案就是新 Worker 还在 waiting。

检查 waiting 状态的代码:

javascript
navigator.serviceWorker.ready.then(reg => { if (reg.waiting) { console.log('新版本等待中:', reg.waiting); } });

激活阶段

旧 Worker 不再控制任何页面后,新 Worker 进入 activate 状态。这一步的核心工作是清理旧缓存

javascript
self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(names => Promise.all( names .filter(name => name !== CACHE_NAME) .map(name => caches.delete(name)) ) ) ); self.clients.claim(); });

clients.claim() 的作用是让新激活的 Worker 立即接管所有未受控制的页面。但已经由旧 Worker 控制的页面不会自动切换——这就是为什么即使调用了 claim(),已有页面仍需要刷新才能使用新逻辑。

生产环境的更新策略

策略一:用户确认更新

这是最稳妥的方式。检测到新版本后提示用户,用户确认后触发更新并刷新:

javascript
let refreshing = false; let newWorker = null; navigator.serviceWorker.register('/sw.js').then(reg => { reg.addEventListener('updatefound', () => { newWorker = reg.installing; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { showUpdatePrompt(); } }); }); }); function onUpdateConfirm() { newWorker.postMessage({ type: 'SKIP_WAITING' }); } navigator.serviceWorker.addEventListener('controllerchange', () => { if (!refreshing) { refreshing = true; window.location.reload(); } }); // sw.js 中 self.addEventListener('message', event => { if (event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } });

注意 refreshing 标志——controllerchange 事件可能触发多次,需要防止重复刷新。

策略二:静默更新

对于非关键更新(比如缓存策略微调),可以不做提示,让新 Worker 自然等待旧页面关闭后激活。用户体验无感知,但更新有延迟。

策略三:定期轮询

浏览器自身的检查依赖用户导航,如果用户长时间停留在 SPA 页面,可能很久都不会触发检查。可以加一层定时轮询:

javascript
setInterval(() => { navigator.serviceWorker.ready.then(reg => reg.update()); }, 60 * 60 * 1000);

但不要把间隔设得太短,否则浪费用户流量和服务器资源。

缓存版本管理

更新过程中缓存管理容易出错。核心原则:每个版本独立缓存,激活时清理旧版本

javascript
const VERSION = 'v2.1.0'; const CACHE_NAME = `app-${VERSION}`; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS)) ); }); self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(names => Promise.all( names.filter(n => n !== CACHE_NAME).map(n => caches.delete(n)) ) ) ); });

常见错误是在 install 阶段就删除旧缓存——如果新 Worker 安装后还没激活就被丢弃了,旧缓存已经被清掉,会导致离线功能失效。清理操作必须放在 activate 阶段。

更新策略对比

策略skipWaiting用户提示适用场景
用户确认用户确认后调用生产环境推荐
静默等待不调用非关键更新
强制更新install 时调用紧急修复、开发环境
定期轮询配合用户确认SPA 长驻页面

调试技巧

开发时更新行为和线上不同,Chrome DevTools 提供了几个调试选项:

  • Update on reload:每次刷新页面都强制检查更新并激活,跳过 waiting
  • Bypass for network:请求绕过 Service Worker,直接走网络
  • SkipWaiting 按钮:在 Application 面板手动激活等待中的新 Worker

代码中查看当前状态:

javascript
navigator.serviceWorker.getRegistration().then(reg => { console.log('active:', reg.active?.state); console.log('waiting:', reg.waiting?.state); console.log('installing:', reg.installing?.state); });

注销 Service Worker(开发调试用,不要在生产环境调用):

javascript
navigator.serviceWorker.getRegistration().then(reg => { reg?.unregister(); });

理解 Service Worker 更新机制的关键在于把握一个原则:更新是非破坏性的,旧版本在页面关闭前始终有效。所有策略和技巧都是围绕如何在这个约束下平衡更新速度与用户体验。

标签:Service Worker