# 更新策略

> 选择新版 Service Worker 触达用户的方式：等待的默认行为、用 skipWaiting() 立即接管、用户确认后刷新，以及周期性更新检查。

完成本指南后，新版本的 Service Worker 会按你选定的方式触达用户，并且有实现该选择的页面代码与 worker 代码：
浏览器的等待默认行为、立即接管，或由用户从「有可用更新」提示中触发的刷新。机制本身（`skipWaiting()`、
`clients.claim()`、waiting 状态）见
[Service worker 更新流程与 skipWaiting](/zh/reference/service-worker/update-skipwaiting/)；
本指南讲的是选哪一种以及如何接线。

你需要一个脚本在每次部署之间会变化的已注册 worker。抓取到的脚本与浏览器持有的逐字节不同时，浏览器就视为
有更新；检查发生在导航到 scope 内页面时、`push` 与 `sync` 事件时（除非前 24 小时内已检查过），
以及脚本 URL 改变后的 `register()` 时。多数浏览器在这个检查中忽略 HTTP 缓存头。

## 保留等待的默认行为

不调用 `skipWaiting()` 时，新安装的 worker 会等到旧 worker 控制的客户端数为零。默认行为换来的是一致性：
页面始终用它加载时的那个 worker 版本，于是它所依赖的缓存就是作答的缓存。代价是刷新不够：刷新期间新旧页面
短暂重叠，旧 worker 仍控制着一个客户端，所以只有用户关闭或离开所有标签页之后更新才生效。
如果用户会在一天内关闭标签页，并且这段时间内的旧版本可以接受，就选它。

```js
// sw.js：什么都不用加。install 处理函数只做预缓存。
self.addEventListener('install', (event) => {
  event.waitUntil(caches.open('shell-v4').then((cache) => cache.addAll(['/', '/app.js'])));
});
```

## 用 skipWaiting() 立即激活

在 `install` 中调用 `self.skipWaiting()`，新 worker 在安装完成后立刻激活，哪怕旧 worker 的页面还开着。
当首次安装就应当控制那些没有 worker 时加载的页面，再在 `activate` 中配上 `clients.claim()`。
代价是：新 worker 开始为旧版本生成的 HTML 作答，所以一个不向后兼容的缓存或 schema 变更会弄坏这些已打开的页面。
用它来发布应当立即生效、且不具破坏性的修复。

```js
// sw.js
self.addEventListener('install', (event) => {
  self.skipWaiting(); // 它返回的 Promise 可以忽略
  event.waitUntil(caches.open('shell-v4').then((cache) => cache.addAll(['/', '/app.js'])));
});

self.addEventListener('activate', (event) => {
  event.waitUntil(self.clients.claim());
});
```

## 让用户决定何时刷新

折中方案是安装新 worker、让它等待、告知用户，只在用户接受时才调用 `skipWaiting()`，随后做一次受控的刷新。
它既避免了无条件 `skipWaiting()` 的隐性不一致，也避免了默认行为「更新迟迟不生效」的代价。
worker 监听一条消息；页面观察注册对象是否出现等待中的 worker，并在 `controllerchange` 时只刷新一次。

```js
// sw.js
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting();
});
```

```js
// 页面
const registration = await navigator.serviceWorker.register('/sw.js');

function offerUpdate(worker) {
  const banner = document.querySelector('#update-banner');
  banner.hidden = false;
  banner.querySelector('button').onclick = () => worker.postMessage({ type: 'SKIP_WAITING' });
}

if (registration.waiting) offerUpdate(registration.waiting);
registration.addEventListener('updatefound', () => {
  const installing = registration.installing;
  installing.addEventListener('statechange', () => {
    if (installing.state === 'installed' && navigator.serviceWorker.controller) offerUpdate(installing);
  });
});

let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
  if (refreshing) return;
  refreshing = true;
  location.reload();
});
```

`navigator.serviceWorker.controller` 这个检查让首次安装时不弹横幅，因为那时没有旧 worker 可替换。

:::observed
上面的横幅可见期间，Chrome DevTools 的 **Application** > **Service workers** 把新 worker 显示为
`waiting to activate`，旁边有一个 **skipWaiting** 链接（英文界面）。若改为勾选 **Update on reload**，
DevTools 会在每次导航时重新抓取脚本、即使逐字节相同也照样安装、跳过等待并导航，于是横幅永远不会出现；
测试这个流程前要取消勾选。
:::

## 在长期打开的标签页中检查更新

一个开了几天的标签页从不导航，所以从不触发浏览器的检查。`registration.update()` 抓取脚本并在不同时安装，
上一次抓取距今超过 24 小时时会绕过 HTTP 缓存。每小时调用一次能让自助终端或仪表盘与最新部署的差距保持在
一小时之内，代价是每个标签页每小时多一个小请求。

```js
navigator.serviceWorker.register('/sw.js').then((registration) => {
  setInterval(() => registration.update(), 60 * 60 * 1000);
});
```

## 按风险匹配策略

| 场景 | 策略 | 代价 |
|---|---|---|
| 用户每天都会关闭或离开标签页 | 等待的默认行为 | 最后一个标签页关闭后更新才生效 |
| 标签页会开上几小时甚至几天 | 默认行为加每小时一次 `registration.update()` | 每个标签页每小时多一个请求 |
| 发布改变了缓存布局或 IndexedDB schema | 默认行为，或用户确认后刷新 | 要设计一条横幅，用户必须接受一次刷新 |
| 应当立即生效的非破坏性修复 | 在 `install` 中调用 `skipWaiting()` | 新 worker 为旧版本构建的页面作答 |

部署一个逐字节有变化的 `sw.js`，在两个标签页里打开应用，在 Service workers 面板里观察所选行为：
默认行为在两个标签页都关闭前一直显示 `waiting to activate`；`skipWaiting()` 在下一次刷新时显示新 worker
`activated and is running`；确认式流程先出横幅，再刷新。

## 另请参阅

- [Service worker 更新流程与 skipWaiting](/zh/reference/service-worker/update-skipwaiting/)
- [Service worker 生命周期：install、activate 与更新](/zh/reference/service-worker/lifecycle/)
- [离线策略](/zh/guides/offline/)
- [The service worker lifecycle](https://web.dev/articles/service-worker-lifecycle)（web.dev）
- [ServiceWorkerGlobalScope: skipWaiting() method](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting)（developer.mozilla.org）
- [ServiceWorkerRegistration: update() method](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/update)（developer.mozilla.org）

← 返回[指南](/zh/guides/)总览。