# Workbox

> Workbox 7 各包做什么，precacheAndRoute() 与 __WB_MANIFEST 如何生成带版本的缓存，GenerateSW 与 InjectManifest 何时适用，以及页面更新。

Workbox 是 Chrome 团队维护的一组 JavaScript 库，实现了 service worker 中原本要手写重复的路由、缓存、预缓存与更新消息代码；当前主版本为 7（2023-04 发布）。它只建立在标准的 Service Worker、Cache 与 Fetch API 之上，因此凡是有 service worker 的浏览器都能运行（Chrome 40、Firefox 44、Safari 11.1、Edge 17；BCD `api.ServiceWorker`），而可选模块在底层 API 缺失时会降级，例如 Firefox 与 Safari 中的 `workbox-background-sync`。

## 工作原理

每个 `workbox-*` 包对应 service worker 的一项任务；worker 脚本只导入用到的部分，既可以经打包器从 npm 导入，也可以从 CDN 以 ES 模块方式导入。

| 包 | 取代手写 worker 中的哪部分 |
|---|---|
| `workbox-routing` | `fetch` 处理器里的 `if`/`else` 链：`registerRoute(matcher, handler)` 与 `NavigationRoute` |
| `workbox-strategies` | 五种策略函数体：`CacheFirst`、`NetworkFirst`、`StaleWhileRevalidate`、`NetworkOnly`、`CacheOnly`，带 `networkTimeoutSeconds` 与插件钩子 |
| `workbox-precaching` | `install`/`activate` 中按内容修订号填充与清理缓存 |
| `workbox-expiration` | 缓存条目数与时长上限（`maxEntries`、`maxAgeSeconds`） |
| `workbox-cacheable-response` | `cache.put()` 之前的 `response.ok` 检查 |
| `workbox-background-sync` | 在 `sync` 事件中重放的 IndexedDB 队列；没有 Background Sync 的浏览器中，队列在 worker 下次启动时重放 |
| `workbox-broadcast-update` | Stale-While-Revalidate 刷新改变了缓存响应时发出的 `BroadcastChannel` 消息 |
| `workbox-window` | 页面侧的注册、更新检测与 `skipWaiting` 消息传递 |

预缓存是无法在运行时手写的那部分。构建工具扫描输出目录，生成一个 `{ url, revision }` 数组并替换 worker 中的 `self.__WB_MANIFEST` 标记；`precacheAndRoute()` 把每个文件存入名为 `workbox-precache-v2-<scope>` 的缓存，以 URL 为键、修订号作为 `__WB_REVISION__` 查询参数附加其后，并为这些 URL 安装一条 Cache First 路由。更新时只抓取修订号变化的条目；`activate` 处理器删除不再出现在清单中的条目。文件名本身已含内容哈希的文件得到 `revision: null`，仅凭 URL 即可标识版本。

两种构建模式都能生成这份清单。`GenerateSW`（在 `workbox-build`、`workbox-cli`、`workbox-webpack-plugin` 与 `vite-plugin-pwa` 中）根据配置写出完整的 worker，在 worker 只需要预缓存与运行时缓存规则时已经够用；`InjectManifest` 接受你自己写的 worker 源文件，替换其中的 `__WB_MANIFEST` 标记，任何自定义的 `fetch`、`push`、`sync` 或 `message` 处理器都需要它。取舍在于：`GenerateSW` 每次构建都重新生成 worker，配置是唯一的事实来源；`InjectManifest` 让 worker 成为普通代码，代价是导入要自己维护。

页面侧，`workbox-window` 的 `Workbox` 类封装了 `register()`，并抛出 `installed`、`waiting`、`controlling`、`activated` 事件；`wb.messageSkipWaiting()` 发送一条 `{ type: 'SKIP_WAITING' }` 消息，`GenerateSW` 生成的 worker 自带对应的 `message` 监听器，`InjectManifest` 的 worker 则要加上第一个示例中那四行监听代码。

:::observed
Workbox 7 的开发构建（打包器把 `process.env.NODE_ENV` 设为 `production` 以外的值，或加载 CDN 上的 `-dev` 文件时启用）会在 Console 中以带颜色的 `workbox` 徽标输出日志：每拦截一次请求就打印一个折叠分组，如 `workbox Router is responding to: /app.js`，展开后的行写明所用策略（`Using CacheFirst to respond to '/app.js'`）以及响应是否来自缓存；安装时打印 `workbox Precaching 42 files. 3 files are already cached.`。生产构建什么也不打印。措辞以来源中的 Workbox "Troubleshooting and logging" 页面为准。
:::

## 示例

第一个示例是一个完整的 `InjectManifest` worker，含预缓存与一条运行时路由；第二个是页面侧代码，包括浏览器没有 service worker 时的分支。

### 带预缓存与图片路由的 InjectManifest worker

`__WB_MANIFEST` 标记在构建时被替换；`ExpirationPlugin` 限制运行时缓存的规模，预缓存则不需要它，因为清单本身已经限定了范围。

```js
// src/sw.js：swSrc 文件，__WB_MANIFEST 由构建工具填入
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';

cleanupOutdatedCaches();                 // 删除旧版预缓存格式留下的缓存
precacheAndRoute(self.__WB_MANIFEST);    // [{ url: '/index.html', revision: 'a1b2' }, …]

self.addEventListener('message', (event) => {
  if (event.data?.type === 'SKIP_WAITING') self.skipWaiting(); // 与 workbox-window 配对
});

registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      new CacheableResponsePlugin({ statuses: [0, 200] }),
      new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 }),
    ],
  })
);
```

`statuses` 中包含 `0` 会缓存 opaque 的跨源图片，Chromium 在配额核算中把每条按约 7 MB 计入；图片为同源或带 CORS 时应去掉 `0`，只存储真正的 `200` 响应。

### 用 workbox-window 注册并检测支持

`Workbox.register()` 兑现为注册对象，或以与 `navigator.serviceWorker.register()` 相同的错误拒绝。`serviceWorker` 不存在时页面继续以仅在线方式工作，所以把导入放在特性检测之后，避免白白加载模块。

```js
// page.js
if ('serviceWorker' in navigator) {
  const { Workbox } = await import('workbox-window');
  const wb = new Workbox('/sw.js');

  wb.addEventListener('waiting', () => {
    showBanner('新版本已就绪。', () => {
      wb.addEventListener('controlling', () => window.location.reload());
      wb.messageSkipWaiting();
    });
  });

  wb.register();
} else {
  // 没有 service worker：不显示更新横幅；应用直接走网络。
}
```

在 `controlling` 事件中刷新，而不是 `messageSkipWaiting()` 之后立即刷新，是为了等新 worker 完成接管，这样刷新后的页面由新的预缓存提供；过早刷新可能加载到旧 HTML 与新资源的混合。

## 另请参阅

- [Workbox: precaching module](https://developer.chrome.com/docs/workbox/modules/workbox-precaching)（developer.chrome.com）
- [Workbox: the ways of Workbox](https://developer.chrome.com/docs/workbox/the-ways-of-workbox)（developer.chrome.com）
- [Workbox: workbox-window module](https://developer.chrome.com/docs/workbox/modules/workbox-window)（developer.chrome.com）
- [Service worker 缓存策略](/zh/reference/service-worker/caching-strategies/)
- [skipWaiting() 与更新流程](/zh/reference/service-worker/update-skipwaiting/)
- [预缓存](/zh/reference/performance/precaching/)
- [Service worker 离线兜底](/zh/reference/service-worker/offline-fallback/)