# 安全移除 service worker（kill switch）

> registration.unregister() 移除的是什么、已被控制的页面为何在卸载前仍由旧 worker 接管，以及替换用的 worker 如何清掉自己的缓存。

一个用缓存响应请求的 service worker，在服务端修好之后仍会继续那样响应，因为决定受控页面拿到什么的是这个 worker，而不是服务端。修复手段是移除注册，而执行这一步的调用是 `ServiceWorkerRegistration.unregister()`。

## 工作原理

移除其实是两件容易混为一谈的事：取消注册，以及释放旧 worker 仍在控制的那些页面。规范把两者明确分开，而后者并不由页面说了算。

### unregister() 以什么值兑现

`unregister()` 不接受参数，返回一个 promise。找不到注册时该 promise 以 `false` 兑现，否则以 `true` 兑现，「irrespective of whether unregistration happened or not (it may not unregister if someone else just called `ServiceWorkerContainer.register()` with the same scope.)」；service worker 会在被注销之前完成正在进行的操作（[ServiceWorkerRegistration: unregister() method](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/unregister)，developer.mozilla.org）。MDN 把该方法记为 Baseline 广泛可用，自 2018 年 4 月起在各浏览器中可用，并且在部分或全部支持它的浏览器中仅在安全上下文可用。

规范的 `Unregister` 算法解释了为什么兑现值几乎说明不了结果。它按任务的 `storage key` 与 `scope url` 取出注册；若为 null，就以 false 兑现任务 promise 并结束。否则移除该键与 scope 对应的 `registration map` 条目，以 true 兑现任务 promise，再调用 `Try Clear Registration`（[Service Workers](https://w3c.github.io/ServiceWorker/)，w3c.github.io）。因此 `true` 报告的是映射条目已被移除，而不是 worker 已经停止。

### 已打开的页面为何仍留着旧 worker

清理是延后的。规范指出，当 `Try Clear Registration` 在那一步没有触发 `Clear Registration` 时，「Clear Registration is tried again when the last client using the registration is unloaded or the extend lifetime promises for the registration's service workers settle」。方法自身那条注释说得更直接：承载受控客户端活动 service worker 的那份注册「is effective until all the service worker clients (including itself) using this service worker registration unload. That is, the `unregister()` method only affects subsequent navigations.」

这正是让 kill switch 看起来失效的那个行为。执行了调用的那个标签页仍挂着旧的控制者，所以它接下来的 fetch 依然走进旧的处理器。结束这段注册效力的，是一次重新加载，或者关闭该标签页。

### unregister() 不会移除什么

`Unregister` 算法作用于 `registration map` 与该注册的那些 worker。缓存响应要通过另一个接口才能触及：`CacheStorage.keys()` 以一个字符串数组兑现，按创建顺序列出该 `CacheStorage` 跟踪的每一个具名 `Cache` 对象（[CacheStorage: keys() method](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage/keys)，developer.mozilla.org），而 `CacheStorage.delete()` 找到与 `cacheName` 匹配的 `Cache` 并删除它、以 `true` 兑现，找不到同名缓存时以 `false` 兑现（[CacheStorage: delete() method](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage/delete)，developer.mozilla.org）。清理可以直接删除一份明确的已知缓存名称列表；只有必须先发现这些名称时才需要调用 `keys()`。

`keys()` 返回的是这个 `CacheStorage` 对象相关名称到缓存映射中的每一个缓存名称，而不是一份归属于被移除 worker 的列表。该删的是被移除应用自己拥有的那些缓存，这也是下面的示例按应用自己的名称前缀去匹配、而不是照搬整份列表的原因。

## 示例

第一个示例在仍能正常加载的页面里移除注册；第二个覆盖的是浏览器只肯运行那个坏掉的 worker 的情况。

### 移除当前客户端 storage key 下的每份注册

`navigator.serviceWorker.getRegistrations()` 以数组形式取回与某个 `ServiceWorkerContainer` 关联的全部 `ServiceWorkerRegistration` 对象（[ServiceWorkerContainer: getRegistrations() method](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/getRegistrations)，developer.mozilla.org），这正是一次调用就能覆盖到当前页面并不知情的某个 scope 上的 worker 的原因。

```js
// 本应用创建的每个缓存都带这个名称前缀，所以这里只允许删掉
// 带该前缀的名字。
const OWNED_CACHE_PREFIX = 'app-';

async function removeAllWorkers() {
  if (!('serviceWorker' in navigator)) {
    // 该上下文没有 worker API：也就没有已注册的东西需要移除。
    return { supported: false, removed: 0 };
  }
  const registrations = await navigator.serviceWorker.getRegistrations();
  const results = await Promise.all(registrations.map((reg) => reg.unregister()));
  const names = await caches.keys();
  const owned = names.filter((name) => name.startsWith(OWNED_CACHE_PREFIX));
  await Promise.all(owned.map((name) => caches.delete(name)));
  return { supported: true, removed: results.filter(Boolean).length, cachesDeleted: owned.length };
}
```

不支持的分支返回一份结构化结果，因为当前上下文没有暴露 Service Worker API。已被旧 worker 控制的页面会一直留着它直到卸载，所以调用方仍需让这些页面重新加载。把清理限制在本应用范围内，靠的就是那个前缀过滤：换成被移除 worker 实际用过的命名方案；若该方案没有共同前缀，就改为维护一份明确的名称列表。

### 部署一个会把自己注销掉的 worker

当已部署的 worker 提供的正是上一个示例所在页面的旧副本时，替换者就只能以 service worker 的身份到场。`self.registration` 返回指向该 worker 自身 `ServiceWorkerRegistration` 对象的引用（[ServiceWorkerGlobalScope: registration property](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/registration)，developer.mozilla.org），而 `skipWaiting()` 让新 worker 不必等旧 worker 的客户端关闭就能激活（[ServiceWorkerGlobalScope: skipWaiting() method](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting)，developer.mozilla.org）。

```js
// sw.js，用来顶替正在被移除的那个 worker
const OWNED_CACHE_PREFIX = 'app-';

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

self.addEventListener('activate', (event) => {
  event.waitUntil((async () => {
    const names = await caches.keys();
    await Promise.all(
      names
        .filter((name) => name.startsWith(OWNED_CACHE_PREFIX))
        .map((name) => caches.delete(name)),
    );
    await self.registration.unregister();
  })());
});
```

这个脚本不注册 `fetch` 处理器，所以一旦它成为活动 worker，它的客户端在下一次导航时就走网络。注册本身由那次 `unregister()` 调用移除，而该注册名下 worker 的清理，按上面引用的规范注释，发生在最后一个使用它的客户端卸载之时。

:::caution
去掉那层过滤，也就是对 `caches.keys()` 的完整结果直接运行 `names.map((name) => caches.delete(name))`，会删除这个 `CacheStorage` 对象相关映射中的每个具名缓存，而不只是这个 worker 拥有的缓存。对 kill switch 本身来说，应把删除范围限制在该应用拥有的缓存名称内。当需要更广的清理时，[Clear-Site-Data](/zh/reference/storage/clearing-data/) 是由服务端驱动的手段。
:::

:::observed
Chrome DevTools 在英文界面下，于 Application › Service workers 暴露同一个操作：按钮文字是 `Unregister`，提示文字是 `Unregister service worker`。两者都是 DevTools 前端 `ServiceWorkersView.ts` 里的 `UIStrings` 条目，说明均为 `Text in Service Workers View of the Application panel`（[ServiceWorkersView.ts](https://chromium.googlesource.com/devtools/devtools-frontend/+/main/front_end/panels/application/ServiceWorkersView.ts)，chromium.googlesource.com）。
:::

## 另请参阅

- [ServiceWorkerRegistration: unregister() method](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/unregister)（developer.mozilla.org）
- [Service Workers](https://w3c.github.io/ServiceWorker/)（w3c.github.io）
- [CacheStorage: delete() method](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage/delete)（developer.mozilla.org）
- [skipWaiting() 与更新流程](/zh/reference/service-worker/update-skipwaiting/)
- [Service worker 生命周期](/zh/reference/service-worker/lifecycle/)
- [Clear-Site-Data 响应头](/zh/reference/storage/clearing-data/)
- [Service worker 调试](/zh/reference/service-worker/debugging/)