# Service worker 调试

> Chrome DevTools、Firefox DevTools 与 Safari Web Inspector 在哪里暴露 service worker 状态，如何强制更新、绕过 worker、模拟离线，附面板原文。

调试 service worker 就是去读页面自己显示不出来的三样东西：注册的状态（installing、waiting、active）、Cache Storage 的内容，以及哪些请求是由 worker 应答的。Chrome 40+ 与 Edge 79+ 在 DevTools › Application 中集中展示这三项；Firefox 44+ 把它们分散在 `about:debugging` 与 Storage 面板；Safari 11.1+ 在 Web Inspector 中列出注册，但没有缓存查看器，也没有强制更新控件（BCD `api.ServiceWorker`；厂商文档见来源）。

## 工作原理

各家面板不同，但每个面板都只是同一个注册对象、同一个 worker 所见 `caches` 的一种视图，因此面板能做的事都可以在 worker 的控制台上下文里完成。

### Chrome 与 Edge DevTools

Application › **Service workers** 列出该源的每条注册，带 scope、脚本 URL 与一行状态，格式为 `#<id> activated and is running`、`#<id> waiting to activate` 或 `#<id> trying to install`。顶部三个复选框只改变当前标签页的行为：**Offline** 让 worker 内与页面中的 `fetch()` 像断网一样失败；**Update on reload** 在每次导航时强制逐字节比较并把抓取到的脚本作为新 worker 安装（绕过 24 小时脚本缓存规则）；**Bypass for network** 把所有请求直接送往网络，不在 worker 中触发 `fetch`。每条注册有 **Update**、**Unregister** 链接，处于等待状态的 worker 旁还有 **skipWaiting** 链接，无需改代码即可对它调用 `skipWaiting()`。Application › **Cache storage** 以 `<缓存名> - <源>` 列出缓存，显示每个条目的请求 URL、响应头与正文预览，并支持按条目或按整个缓存删除。Network 面板中，由 worker 产生的响应在 **Size** 列显示 `(ServiceWorker)`，worker 自己发出的请求带齿轮图标。Console 面板左上角的上下文选择器（默认 `top`）以脚本 URL 列出 worker；选中后，worker 内的 `console.log` 会显示在这里，也可以在 worker 的全局作用域中执行 `await caches.keys()` 或 `await self.clients.matchAll()`。

### Firefox DevTools

`about:debugging#/runtime/this-firefox` 在 **Service Workers** 下列出注册，带 scope、状态与两个按钮：**Inspect** 打开一个以 worker 为上下文的工具箱（其 Console 就是 worker 控制台），**Unregister** 注销。页面的工具箱也在 Application › Service Workers 中显示该 worker（Firefox 79+），已停止的 worker 旁有 **Start** 按钮。Firefox 没有"重新加载时更新"开关；等价做法是在页面控制台调用 `registration.update()`，或先 **Unregister** 再刷新。Cache Storage 出现在 Storage 面板的 **Cache Storage** 下，列出各缓存及其请求 URL，并可删除条目。

### Safari Web Inspector

启用 Develop 菜单（Safari › 设置 › 高级 › **Show features for web developers**，Safari 17 之前的标签为 **Show Develop menu in menu bar**），再打开 Develop › **Service Workers**，它按 scope 列出注册，并打开一个附着到 worker 上下文的 Web Inspector 窗口。Storage 标签显示 IndexedDB 与 Local Storage，但不显示 Cache Storage，因此要列出缓存条目只能在 worker 控制台执行 `await caches.keys()` 与 `await (await caches.open(name)).keys()`。没有离线模拟与强制更新；改用 Network Link Conditioner（macOS）或飞行模式（iOS），以及 `registration.update()`。

### 读症状

下面每种故障都在某个面板中有可见的特征。

| 症状 | 显示位置 | 原因与修法 |
|---|---|---|
| 新代码没有运行 | Chrome：第二个 worker 旁显示 `waiting to activate` | 旧标签页仍被控制；关闭它们、点 **skipWaiting**，或在代码中提供 `skipWaiting()` 路径 |
| 部署被忽略最多一天 | Network 面板显示脚本来自 `(disk cache)` | 脚本被 HTTP 缓存；注册时传 `updateViaCache: 'none'`，或给脚本 URL 加 `Cache-Control: no-cache` |
| 请求没被拦截 | Network 的 **Size** 列没有 `(ServiceWorker)` | 页面在 scope 之外、worker 尚未激活，或开着 **Bypass for network** |
| 安装后缓存为空 | Cache storage 没有缓存，worker 控制台显示 `addAll()` 被拒绝 | 某个预缓存 URL 返回了非 2xx 状态；`addAll()` 是原子的，任一失败则什么都不存 |
| 安装卡住 | 状态停在 `trying to install` | `event.waitUntil()` 收到了一个永不落定的 Promise |

:::observed
在 Chrome DevTools（英文界面）中，Application › Service workers 把刚部署的第二个版本显示为运行中 worker 正下方的 `#<id> waiting to activate`，旁边的 **skipWaiting** 链接可以不刷新页面就激活新 worker；同一页面在 Firefox 中要先在 `about:debugging` 点 **Inspect** 才能看到两个版本，而 Safari 的 Develop › Service Workers 每个 scope 只列一项、没有状态文字。面板标签以来源中 Chrome DevTools "Debug Progressive Web Apps" 指南与 Firefox `about:debugging` 文档为准。
:::

## 示例

下面两段代码用脚本重现最常用的两个面板操作，因此在没有对应按钮的浏览器中同样可用。

### 在页面控制台强制更新检查并列出缓存内容

`registration.update()` 执行的正是 "Update on reload" 触发的那次逐字节比较；页面中的 `caches.keys()` 列出的也正是 Application 面板显示的那些缓存，因为同一源上的页面与 worker 共享 `caches`。

```js
const registration = await navigator.serviceWorker.getRegistration();
await registration.update();                 // 抓取 /sw.js，有差异则安装
console.log(registration.waiting?.state);    // 有更新在等待时为 "installed"

for (const name of await caches.keys()) {
  const cache = await caches.open(name);
  const keys = await cache.keys();
  console.log(name, keys.map((request) => request.url));
}
```

页面侧的列举在没有缓存查看器的 Safari 中同样有效；代价是 opaque 响应只能看到 URL，它们的正文在任何上下文中都读不出来。

### 用一个调试开关记录每次 fetch 决策，生产环境下为空操作

在 `fetch` 处理器里打日志，是 Workbox 开发构建日志的手工版本。以注册 URL 为开关可以让生产 worker 保持安静且无需构建步骤；`console` 不可用时（较老的 WebView）守卫直接什么都不做。

```js
// sw.js
const DEBUG = new URL(self.location.href).searchParams.has('debug'); // register('/sw.js?debug')

self.addEventListener('fetch', (event) => {
  event.respondWith((async () => {
    const cached = await caches.match(event.request);
    if (DEBUG && typeof console !== 'undefined') {
      console.log(cached ? 'cache' : 'network', event.request.method, event.request.url);
    }
    return cached ?? fetch(event.request);
  })());
});
```

注册 `/sw.js?debug` 安装的是另一个脚本 URL，因而是另一个 worker；测量前记得改回不带查询串的注册，否则带日志的 worker 会一直控制该标签页。

## 另请参阅

- [Debug Progressive Web Apps](https://developer.chrome.com/docs/devtools/progressive-web-apps/)（developer.chrome.com）
- [about:debugging](https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/index.html)（firefox-source-docs.mozilla.org）
- [Workbox: troubleshooting and logging](https://developer.chrome.com/docs/workbox/troubleshooting-and-logging)（developer.chrome.com）
- [skipWaiting() 与更新流程](/zh/reference/service-worker/update-skipwaiting/)
- [Service worker 生命周期](/zh/reference/service-worker/lifecycle/)
- [Cache API](/zh/reference/service-worker/cache-api/)
- [Service worker 注册与 scope](/zh/reference/service-worker/registration-scope/)