# Service worker 更新流程与 skipWaiting

> 浏览器如何检测新版本 service worker、waiting 状态的含义，以及如何通过 skipWaiting() 和 clients.claim() 选择立即接管。

**一句话：** 当浏览器发现字节不同的 service worker 脚本时，它会将新版本安装在旧版本旁边，并将其保持在 *waiting* 状态，直到所有受控标签页关闭——除非你调用 `self.skipWaiting()` 跳过这个等待。

## 浏览器如何检测更新

浏览器会在每次导航（scope 范围内）以及显式调用 `registration.update()` 时自动重新获取 worker 脚本。若获取到的脚本与当前已注册的版本哪怕只有一个字节不同，浏览器就会开始新的安装流程。

更新检查有一条特殊的新鲜度规则：对于距上次检查超过 24 小时的 worker 脚本，即使服务器设置了激进的 `Cache-Control` 头，浏览器也会绕过 HTTP 缓存。注册时使用 `updateViaCache: 'none'` 选项可让每次检查都完全绕过缓存。

## Waiting 状态

新 worker 成功完成安装（`install` 事件 resolve）后，进入 *waiting* 状态。在以下条件满足之前，它不会激活，也不会处理 `fetch` 事件：

1. 被 *旧* worker 控制的所有标签页全部关闭，**或**
2. 新 worker 调用 `self.skipWaiting()`。

这一设计保证了页面与为其提供服务的 worker 始终来自同一版本。运行旧版 HTML 的标签页不会被交给可能使用不同缓存键或 API 的新 worker 接管。

## `self.skipWaiting()`

在 `install` 事件中调用 `skipWaiting()` 会告知浏览器立即激活新 worker，即使旧版本仍在控制打开的标签页：

```js
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('app-v2').then((cache) => cache.addAll(['/index.html', '/app.js']))
  );
  self.skipWaiting(); // 安装后立即激活
});
```

`skipWaiting()` 是异步的，但其效果相对于当前任务通常是即时的。在 `install` 外部调用也有效，但放在 `waitUntil` 内部是最可靠的做法。

**风险：** 若新 worker 的缓存资源与旧 worker 已加载的 HTML 不兼容，在不刷新所有标签页的情况下调用 `skipWaiting()` 可能引发运行时错误。务必搭配 `controllerchange` 监听器一起使用。

## `clients.claim()`

默认情况下，新激活的 worker 不会控制注册时已打开的页面——这些页面在当前生命周期内处于无 worker 控制状态。`clients.claim()` 让新 worker 立即接管这些页面：

```js
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => k !== 'app-v2').map((k) => caches.delete(k)))
    )
  );
  self.clients.claim(); // 接管 scope 内所有打开的标签页
});
```

只有在有明确理由时才使用 `clients.claim()`——例如，确保注册后首次加载即受控（使离线功能立即生效）。不必要地使用 `claim()` 会导致所有当前打开的标签页在会话中途更换 worker。

## 通知用户

当 `skipWaiting()` 激活新 worker 后，现有标签页仍在运行旧代码。推荐的做法是在页面中监听 `controllerchange` 并提示用户刷新：

```js
// 在页面中
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
  if (refreshing) return;
  refreshing = true;
  window.location.reload();
});
```

也可以使用 `registration.waiting` 属性检测待更新状态，展示"刷新以更新"的 UI，而非强制自动刷新。

## 手动触发更新检查

调用 `registration.update()` 可立即检查是否有新版本 worker 脚本：

```js
navigator.serviceWorker.ready.then((registration) => {
  registration.update();
});
```

原生 Service Workers API 需要你自行调用 `registration.update()` 以实现主动更新检测。若使用 Workbox，其 `workbox-window` 包提供了 `wb.addEventListener('waiting', ...)` 用于响应更新事件，但定期重新检查仍需显式调用 `wb.update()` 或依赖浏览器导航时自动触发的检查。

## 实践清单

- [ ] 注册时使用 `updateViaCache: 'none'`，确保浏览器始终重新获取 worker 脚本。
- [ ] 若使用 `skipWaiting()`，同时在页面中添加 `controllerchange` 监听器以刷新标签页，避免版本不一致。
- [ ] 在 `activate` 事件中清理旧缓存名称，确保在 `skipWaiting()` 让新 worker 激活后及时清理。
- [ ] 对关键应用，考虑展示可见的"有可用更新——点击刷新"提示，而非静默自动刷新。
- [ ] 通过"加载页面→修改 worker→导航（非刷新）"流程测试更新行为，而非仅在 DevTools 中强制刷新。

## 相关参考

- [Service Worker 生命周期](/zh/reference/service-worker/lifecycle/) — 完整的 install / activate / waiting 流程
- [注册与 scope](/zh/reference/service-worker/registration-scope/) — `register()` 的 `updateViaCache` 选项
- [Service Worker 调试](/zh/reference/service-worker/debugging/) — 使用 DevTools 强制更新并检查 waiting 中的 worker