# Service worker 生命周期

> service worker 从 register() 到 redundant 经过哪些状态、每次转换触发什么事件、新版本为何等待，以及 skipWaiting() 改变什么。

import Figure from '@components/Figure.astro';
import lifecycleDiagram from '@assets/diagrams/service-worker-lifecycle.svg';

service worker 在处理任何 `fetch` 事件之前，要按固定顺序经过 `installing` → `installed` → `activating` → `activated` 四个状态；新版本会在旧版本旁边安装，但停留在 `installed`（waiting）状态，直到被旧 worker 控制的标签页全部关闭。这套状态机在 Chrome 40、Firefox 44、Safari 11.1 与 Edge 17 中完全一致（BCD `api.ServiceWorker.state`），浏览器之间的差别只在于观察它的工具。

<Figure src={lifecycleDiagram} alt="Service worker 生命周期流程图：页面调用 navigator.serviceWorker.register()，worker 依次经历 parsed、installing（install 事件）、installed（waiting）、activating（activate 事件）到 activated，可用 clients.claim() 接管已打开页面；self.skipWaiting() 跳过等待，安装失败或被替换的 worker 进入 redundant，更新检查会让新 worker 重新进入同一组状态。" caption="Service worker 的生命周期状态，以及在状态之间推动 worker 的事件与方法。" />

## 工作原理

`navigator.serviceWorker.register(scriptURL)` 抓取并解析脚本。解析成功后，浏览器创建一个状态为 `installing` 的 `ServiceWorker` 对象，在 `ServiceWorkerRegistration` 上触发 `updatefound`，并把该 worker 暴露为 `registration.installing`。之后每一次状态转换都会在这个 `ServiceWorker` 对象上触发 `statechange`，因此页面用一个监听器就能跟完整个序列。

| 状态 | worker 内的事件 | 浏览器的动作 |
|---|---|---|
| `installing` | `install` | 运行处理函数；`event.waitUntil(promise)` 让状态停留到 Promise 落定。拒绝会使 worker 变为 `redundant`，旧版本原地保留。 |
| `installed` | 无 | worker 成为 `registration.waiting`。若没有其他 worker 正在控制客户端，它立刻进入下一步；否则等到最后一个受控客户端关闭，或 worker 自己调用 `self.skipWaiting()`。 |
| `activating` | `activate` | 运行处理函数，同样由 `waitUntil()` 保持。旧缓存在这里删除，因为上一版 worker 已不可能再运行。 |
| `activated` | `fetch`、`message`、`push`、`sync`…… | worker 成为 `registration.active`，控制 scope 内的新导航。已打开的页面继续沿用原控制者，除非 worker 调用 `self.clients.claim()`，这会在每个受影响页面的 `navigator.serviceWorker` 上触发 `controllerchange`。 |
| `redundant` | 无 | 安装失败，或已被更新的 worker 取代。对象在页面释放之前仍可访问。 |

两条规则让这个序列对更新是安全的。等待规则保证页面与为它服务的 worker 来自同一次部署：仍在运行旧 HTML 的标签页不会被交给缓存名不同的 worker。空闲规则意味着 worker 不是常驻进程：事件之间浏览器会终止它（按 web.dev 生命周期文章所述，Chrome 在空闲约 30 秒后终止），下一个事件再重新启动，所以需要留存的数据都要放进 Cache Storage 或 IndexedDB，而不是全局变量。

开启新一轮周期的更新检查发生在每次进入 scope 的导航、`registration.update()` 调用以及 `push` 与 `sync` 事件时，它逐字节比较抓取到的脚本与已安装脚本。相差一个字节就会启动新的 `installing` worker；完全相同则什么也不做。浏览器对该脚本只遵守最多 24 小时的 HTTP 缓存（注册选项 `updateViaCache` 控制主脚本与导入脚本是否允许来自 HTTP 缓存）。

:::observed
Chrome DevTools（英文界面）› Application › Service workers 把每个 worker 的状态打印为 `#<id> activated and is running`、`#<id> waiting to activate` 或 `#<id> trying to install`，并提供 `Update`、`Unregister` 链接，等待中的 worker 旁还有 `skipWaiting` 链接；面板顶部有 **Offline**、**Update on reload**、**Bypass for network** 三个复选框。这些标签记录在来源列表中的 Chrome DevTools "Debug Progressive Web Apps" 页面。
:::

## 示例

第一个示例记录每一次状态转换，让这个序列可以直接在页面控制台里读到；第二个示例选定更新策略，并在 API 缺失时平稳降级。

### 在页面中记录每次状态转换

`updatefound` 对每个新 worker 触发一次；`statechange` 在该 worker 的每次转换时触发；`controllerchange` 在页面的控制者更换时触发。三者合起来就是上表在运行时的重现。

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

registration.addEventListener('updatefound', () => {
  const worker = registration.installing;
  console.log('new worker:', worker.state);          // "installing"
  worker.addEventListener('statechange', () => {
    console.log('state ->', worker.state);          // installed、activating、activated | redundant
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  console.log('controller is now', navigator.serviceWorker.controller?.scriptURL);
});
```

首次访问时，除非 worker 调用了 `clients.claim()`，`controllerchange` 不会触发：页面加载时没有控制者，并一直保持到下一次导航。首次激活后刷新页面，`navigator.serviceWorker.controller` 从一开始就有值。

### 选定更新策略，并为没有该 API 的浏览器准备回退

默认的等待行为就是安全策略，不需要任何代码。下面的代码只在页面准备好刷新时才选择立即接管；当 `serviceWorker` 不存在（非安全源，或浏览器禁用了该 API）时，让应用停留在仅在线模式，而不是抛错。

```js
// page.js
if (!('serviceWorker' in navigator)) {
  document.documentElement.dataset.offline = 'unsupported'; // 仅在线 UI；不显示更新提示
} else {
  navigator.serviceWorker.register('/sw.js').then((registration) => {
    registration.addEventListener('updatefound', () => {
      registration.installing.addEventListener('statechange', (e) => {
        if (e.target.state === 'installed' && navigator.serviceWorker.controller) {
          showReloadBanner(() => registration.waiting.postMessage('SKIP_WAITING'));
        }
      });
    });
  });
  let reloading = false;
  navigator.serviceWorker.addEventListener('controllerchange', () => {
    if (!reloading) { reloading = true; location.reload(); }
  });
}

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

立即接管策略的代价是每次更新强制刷新一次，并且必须保留 `controllerchange` 守卫，否则第二次 `controllerchange`（例如来自 `clients.claim()`）会让页面刷新两次。默认策略的代价是一直不关最后一个标签页的用户会停留在旧版本，直到关闭它为止。

## 另请参阅

- [Service Workers specification: Service worker lifetime](https://w3c.github.io/ServiceWorker/#service-worker-lifetime)（w3c.github.io）
- [The service worker lifecycle](https://web.dev/articles/service-worker-lifecycle)（web.dev）
- [ServiceWorker: state property](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/state)（developer.mozilla.org）
- [skipWaiting() 与更新流程](/zh/reference/service-worker/update-skipwaiting/)
- [Service worker 注册与 scope](/zh/reference/service-worker/registration-scope/)
- [Service worker 调试](/zh/reference/service-worker/debugging/)
- [Clients API](/zh/reference/service-worker/clients-api/)