# 选择缓存策略

> 按资源类型为 Service Worker 选择缓存策略：缓存优先、网络优先、stale-while-revalidate、仅网络或仅缓存，以及每种策略的代价。

完成本指南后，Service Worker 拦截的每一类请求都有一个点名的策略，五种策略都变成可以直接粘进 `sw.js`
的普通函数，而且你清楚每一种在新鲜度、速度或离线可用性上的代价。怎么选取决于资源是静态、动态还是时间敏感的，
以及用户能忍受多旧的响应。

你需要一个带 `fetch` 处理函数的 Service Worker（把请求路由到这些函数的处理函数见
[离线策略](/zh/guides/offline/)）。这些函数使用的 `Cache` 接口与 HTTP 缓存是两回事：`Cache-Control`
头决定浏览器 HTTP 缓存保留什么，对 `cache.put()` 存什么、`caches.match()` 返回多久没有任何影响。

## 核心策略

每个函数接收一个 `Request`、返回一个 `Response`，所以 `fetch` 处理函数可以写成
`event.respondWith(strategy(event.request))`。`cache.put()` 会消耗响应体，所以每个函数都先 `clone()` 再存。

### 缓存优先

先查缓存；只在未命中时走网络，并把结果存起来。

```js
async function cacheFirst(request, cacheName = 'static') {
  const cached = await caches.match(request);
  if (cached) return cached;
  const response = await fetch(request);
  if (response.ok) {
    const cache = await caches.open(cacheName);
    cache.put(request, response.clone());
  }
  return response;
}
```

用于带哈希版本号、同一 URL 下永不改变的资源（JavaScript 包、CSS、图片、字体）。响应即时且离线可用；
代价是缓存条目在 Service Worker 版本变化或条目被删除之前永远不会刷新。

### 网络优先

先试网络；请求失败时回退到缓存副本。

```js
async function networkFirst(request, cacheName = 'dynamic', timeoutMs = 3000) {
  const cache = await caches.open(cacheName);
  try {
    const response = await Promise.race([
      fetch(request),
      new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeoutMs)),
    ]);
    if (response.ok) cache.put(request, response.clone());
    return response;
  } catch {
    const cached = await cache.match(request);
    if (cached) return cached;
    throw new Error(`no network and no cached copy for ${request.url}`);
  }
}
```

用于在线时需要最新版本、离线时上一次看到的版本仍有用的 HTML 与 API 响应。代价是每次请求都要等一次完整的
网络往返，在不稳定的网络上用户得先等到失败缓存才作答；上面的超时限制了这段等待。

### stale-while-revalidate

立即返回缓存的响应，同时在后台刷新缓存供下次使用。

```js
async function staleWhileRevalidate(request, cacheName = 'dynamic') {
  const cache = await caches.open(cacheName);
  const cached = await cache.match(request);
  const refresh = fetch(request).then((response) => {
    if (response.ok) cache.put(request, response.clone());
    return response;
  });
  return cached || refresh;
}
```

用于要快、要大致新鲜、但落后一个版本也无妨的内容：头像、文章列表、UI 框架。代价是每次加载多一个请求，
以及恰好旧了一次访问的响应。

### 仅网络

绝不查缓存。在 `fetch` 处理函数里这意味着根本不调用 `respondWith()`，浏览器就像没有 worker 一样处理请求。

```js
self.addEventListener('fetch', (event) => {
  if (event.request.method !== 'GET') return; // 仅网络：POST、PUT、DELETE
});
```

用于非 GET 请求、实时数据，以及任何绝不能过期的内容。按定义它离线时不工作。

### 仅缓存

只从缓存提供，未命中即失败。

```js
async function cacheOnly(request) {
  const cached = await caches.match(request);
  if (cached) return cached;
  return new Response('Not precached', { status: 504 });
}
```

用于在 `install` 中预缓存的资源：应用外壳与离线回退页。代价是下一个 worker 版本预缓存新的一套之前什么都不会更新。

## 为每类资源匹配策略

| 资源类型 | 策略 | 原因 |
|---|---|---|
| 带哈希文件名的应用外壳（HTML、CSS、JavaScript） | 缓存优先 | 同一 URL 下不可变；首次访问后即时 |
| 图片与字体 | 缓存优先 | 很少变化；体积大到重新抓取很伤 |
| 导航请求 | 网络优先，配离线回退页 | 在线时 HTML 新鲜；离线时用缓存外壳或回退页 |
| 用户专属的 API 数据 | 网络优先 | 必须新鲜；缓存是离线兜底 |
| 共享或半静态的 API 数据 | stale-while-revalidate | 先从缓存绘制，为下次刷新 |
| POST、PUT、DELETE、WebSocket 升级 | 仅网络 | 无法从缓存提供 |
| 预缓存的离线页 | 仅缓存 | 存在的意义就是回退永不依赖网络 |

Workbox 在 `workbox-strategies` 中以类的形式提供同样五种：`CacheFirst`、`NetworkFirst`、
`StaleWhileRevalidate`、`NetworkOnly` 与 `CacheOnly`，用 `registerRoute()` 按路由注册。`NetworkFirst`
与 `NetworkOnly` 接受 `networkTimeoutSeconds` 选项，作用相当于上面的超时；Workbox 的 `StaleWhileRevalidate`
无论缓存条目多新都会发出重新验证请求。相比自己拥有的手写函数，这个库的代价是一个构建步骤和几 KB 体积。

## 按时间让条目过期

Cache Storage 自身没有过期机制，一个月前存的响应会被 `caches.match()` 原样返回。对既非不可变也非关键的资源，
检查响应的 `Date` 头，把过旧的条目当作未命中。

```js
async function cacheWithExpiry(request, maxAgeSeconds, cacheName = 'dynamic') {
  const cache = await caches.open(cacheName);
  const cached = await cache.match(request);
  if (cached) {
    const storedAt = new Date(cached.headers.get('date') || 0).getTime();
    if (Date.now() - storedAt < maxAgeSeconds * 1000) return cached;
    await cache.delete(request);
  }
  const response = await fetch(request);
  if (response.ok) cache.put(request, response.clone());
  return response;
}
```

`Date` 头由服务器设置，任何一侧的时钟不准都会让过期时间偏移；把自己的时间戳连同缓存键存进 IndexedDB
更麻烦也更精确。Workbox 的 `ExpirationPlugin` 用 `maxAgeSeconds` 与 `maxEntries` 做这件事。

:::observed
在 Chrome DevTools 中，**Application** > **Storage** > **Cache Storage**（英文界面）列出每个命名缓存；
选中一个会以表格显示其条目，选中一条会在表格下方显示存储的 HTTP 头，正文在 **Preview** 标签页里。
一条以 `Cache-Control: max-age=60` 存入的条目在六十秒过去很久之后仍列在其中、仍由 `caches.match()` 返回，
因为该头管的是 HTTP 缓存而不是这个存储。**Delete Selected** 删除一条；Storage 视图上的 **Clear site data**
删除该源的全部缓存。
:::

## 另请参阅

- [离线策略](/zh/guides/offline/)
- [Service worker 缓存策略](/zh/reference/service-worker/caching-strategies/)
- [Cache Storage：离线 PWA 背后的请求/响应存储](/zh/reference/storage/cache-storage/)
- [Workbox](/zh/reference/service-worker/workbox/)
- [Strategies for service worker caching](https://developer.chrome.com/docs/workbox/caching-strategies-overview)（developer.chrome.com）
- [Service worker caching and HTTP caching](https://web.dev/articles/service-worker-caching-and-http-caching)（web.dev）
- [workbox-strategies](https://developer.chrome.com/docs/workbox/modules/workbox-strategies)（developer.chrome.com）

← 返回[指南](/zh/guides/)总览。