# Service worker fetch 事件与路由

> FetchEvent 接口的工作原理、如何通过 event.respondWith() 响应请求，以及将请求路由到不同处理器的模式。

**一句话：** 页面在 service worker scope 范围内发起的每一个网络请求，都会在 worker 中触发一个 `fetch` 事件。`event.respondWith(promise)` 让 worker 拦截请求并返回任意 `Response`——来自缓存、网络或内联构造。

## FetchEvent 接口

`FetchEvent` 包含以下内容：

- `event.request` — `Request` 对象，包含 `url`、`method`、`headers`、`destination` 和 `mode`。
- `event.respondWith(responsePromise)` — 调用此方法以接管响应。若不调用，请求会像往常一样直接走网络。
- `event.waitUntil(promise)` — 在事件结束后延长 worker 的生命周期，用于缓存预热等副作用操作。
- `event.clientId` — 发出请求的客户端（标签页）标识符。
- `event.resultingClientId` — 若导航请求创建了新客户端，为新客户端的 ID。

## 基本拦截

```js
self.addEventListener('fetch', (event) => {
  // 只拦截同源请求
  if (!event.request.url.startsWith(self.location.origin)) return;

  event.respondWith(
    caches.match(event.request).then((cached) => cached ?? fetch(event.request))
  );
});
```

不调用 `respondWith()` 直接返回，请求会穿透到浏览器正常的网络栈。

## 用于路由决策的请求属性

| 属性 | 取值与含义 |
|---|---|
| `request.destination` | `'document'`、`'script'`、`'image'`、`'font'`、`'fetch'`、`''`（XHR/fetch 无类型） |
| `request.mode` | `'navigate'`（页面导航）、`'cors'`、`'no-cors'`、`'same-origin'` |
| `request.method` | `'GET'`、`'POST'`、`'PUT'` 等 |
| `request.url` | 完整 URL 字符串；使用 `new URL(event.request.url)` 进行路径匹配 |

## 路由模式

### 按 destination 路由

```js
self.addEventListener('fetch', (event) => {
  const { destination } = event.request;

  if (destination === 'image') {
    event.respondWith(cacheFirst(event.request, 'images'));
  } else if (destination === 'document') {
    event.respondWith(networkFirst(event.request, 'pages'));
  }
  // 其他请求透传
});
```

### 按 URL 前缀路由

```js
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);

  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkOnly(event.request));
  } else if (url.pathname.startsWith('/static/')) {
    event.respondWith(cacheFirst(event.request, 'static'));
  }
});
```

### 导航请求（单页应用离线支持）

对于单页应用，所有 `navigate` 请求应返回已缓存的应用外壳：

```js
self.addEventListener('fetch', (event) => {
  if (event.request.mode === 'navigate') {
    event.respondWith(
      caches.match('/index.html').then((cached) => cached ?? fetch(event.request))
    );
    return;
  }
  // 处理其他请求类型…
});
```

## 内联构造 Response

`respondWith()` 接受任意 `Response`，包括你自己构造的：

```js
event.respondWith(
  new Response('<h1>离线中</h1>', {
    headers: { 'Content-Type': 'text/html' },
  })
);
```

这是离线兜底页面的基础。详见[离线兜底](/zh/reference/service-worker/offline-fallback/)。

## `respondWith` 无法覆盖的情况

- **不可拦截的请求。** 来自 `<link rel="preload">` 或第三方 iframe 的 `mode: 'no-cors'` 请求，根据上下文可能不触发 `fetch` 事件。
- **不透明响应。** 以 `no-cors` 方式获取的跨域请求会产生不透明响应（`status: 0`）。可以缓存并返回它们，但无法读取其 body 或状态码。
- **POST 及写操作请求。** `fetch` 事件对 POST 也会触发，但缓存写操作响应需要谨慎设计（离线推迟写操作请参阅后台同步）。

## 使用 Workbox 进行路由

Workbox 的 `registerRoute` 比在 `fetch` 监听器中写长 `if`/`else` 链更清晰：

```js
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst } from 'workbox-strategies';

registerRoute(({ request }) => request.destination === 'image', new CacheFirst());
registerRoute(({ request }) => request.mode === 'navigate', new NetworkFirst());
```

完整详情请参阅 [Workbox](/zh/reference/service-worker/workbox/)。

## 实践清单

- [ ] 只有在确实要处理请求时才调用 `event.respondWith()`；其他请求放行透传。
- [ ] 在缓存前检查 `event.request.method`——除非有明确需要，否则不要缓存非 GET 响应。
- [ ] 使用 `new URL(event.request.url).origin` 区分同源与跨域请求，再进行路由。
- [ ] 处理缓存与网络均失败的情况——返回兜底 `Response`，避免空白错误页面。
- [ ] 将 DevTools 网络节流设置为"离线"来测试路由逻辑。

## 相关参考

- [缓存策略](/zh/reference/service-worker/caching-strategies/) — `respondWith()` 所调用的各种策略
- [Cache API](/zh/reference/service-worker/cache-api/) — 处理器内部使用的 `caches.match()` 等方法
- [离线兜底](/zh/reference/service-worker/offline-fallback/) — 当一切失败时构造有意义的响应
- [Workbox](/zh/reference/service-worker/workbox/) — 生产可用的路由与策略库