# Cache API

> caches 背后的 CacheStorage 与 Cache 接口及其成员，add()、addAll()、put() 抛错的条件，以及 install 与 activate 中的版本化缓存模式。

Cache API 是一个按源隔离、持久保存 `Request`/`Response` 对的存储，在 service worker 与页面中都通过 `caches` 全局对象（一个 `CacheStorage`）访问。除了你的代码写入或删除、以及存储压力下整源被驱逐之外，里面的内容不会变化。它自 Chrome 43、Edge 16、Firefox 41、Safari 11.1 起可用，且仅限安全上下文（BCD `api.CacheStorage`）。

## 语法

```js
// caches 全局对象
const cache = await caches.open(cacheName);
const hit = await caches.match(request, options);
const exists = await caches.has(cacheName);
const names = await caches.keys();
const removed = await caches.delete(cacheName);

// Cache
await cache.add(request);
await cache.addAll([request1, request2]);
await cache.put(request, response);
const response = await cache.match(request, options);
const responses = await cache.matchAll(request, options);
const requests = await cache.keys(request, options);
const deleted = await cache.delete(request, options);
```

所有方法都返回 Promise。`request` 参数可以是 `Request`，也可以是 URL 字符串，后者会相对于 worker 或页面的地址解析。`caches.open()` 在缓存不存在时会创建它。

## 成员

`CacheStorage` 管理带名字的缓存；`Cache` 保存其中一个缓存的条目。

| 成员 | 返回值 | 行为 |
|---|---|---|
| `caches.open(name)` | `Promise<Cache>` | 打开或创建指定名字的缓存。名字是任意字符串，这正是版本化命名（`shell-v3`）得以成立的原因。 |
| `caches.match(request, options)` | `Promise<Response \| undefined>` | 按创建顺序搜索该源的所有缓存，以第一个命中兑现。`options.cacheName` 可把搜索限定在一个缓存内。 |
| `caches.has(name)`、`caches.keys()`、`caches.delete(name)` | `Promise<boolean>`、`Promise<string[]>`、`Promise<boolean>` | 对整个缓存做盘点与删除。名字不存在时 `delete()` 兑现为 `false`。 |
| `cache.add(request)` | `Promise<void>` | 发起请求并存储响应，相当于 `fetch()` 加 `put()`。 |
| `cache.addAll(requests)` | `Promise<void>` | 发起全部请求并原子性地存储：任一失败则一条都不存。 |
| `cache.put(request, response)` | `Promise<void>` | 存储你手头已有的响应，不发请求。body 流会被消耗，若还要把响应返回出去，先 `clone()`。 |
| `cache.match(request, options)` | `Promise<Response \| undefined>` | 在本缓存中查找一个条目。 |
| `cache.matchAll(request, options)` | `Promise<Response[]>` | 匹配该请求的全部条目；省略 `request` 时返回所有条目。 |
| `cache.keys(request, options)` | `Promise<Request[]>` | 已存储的 `Request` 对象，便于按 URL 清理。 |
| `cache.delete(request, options)` | `Promise<boolean>` | 删除匹配的条目；至少删掉一条时兑现为 `true`。 |

`options` 是 `CacheQueryOptions` 字典：`ignoreSearch` 比较时忽略查询串，`ignoreMethod` 允许非 GET 请求匹配已存的 GET 条目，`ignoreVary` 跳过 `Vary` 头的匹配。条目以 URL 与方法为键，并且默认遵守已存响应的 `Vary` 头，所以 `Accept-Language` 不同的请求可能查不到一条在 DevTools 里明明存在的条目。

## 异常

拒绝原因是 `DOMException` 或 `TypeError`。

- `TypeError`：请求方法不是 `GET`（`add`、`addAll`、`put`），请求 scheme 不是 `http:` 或 `https:`，响应状态为 `206`，或其 `Vary` 头含 `*`。Chromium 的报错包括 `Partial response (status code 206) is unsupported` 与 `Vary header contains *`。
- `add()` 与 `addAll()` 的 `TypeError`：某个响应不是 `ok`（状态不在 200 到 299 之间）或请求失败。Chromium 报 `Request failed`，因此预缓存列表里一个 404 就会让整个 `addAll()` 中止；若它运行在 `install` 的 `waitUntil()` 里，安装也随之失败。
- `QuotaExceededError`：`put()`、`add()`、`addAll()` 写入时该源的存储配额已耗尽。Chromium 在配额核算中把每条 opaque（`no-cors`、状态 `0`）响应按约 7 MB 计入，所以在接近满盘的设备上，几条第三方资源就可能触发它。
- `SecurityError`：在非安全上下文访问 `caches`。在 `localhost` 以外的 `http:` 源上，`window.caches` 为 `undefined`，worker 中的 `self.caches` 也不可用。

:::observed
在 Chrome（英文界面）中，预缓存列表里有一个 URL 返回 404 时，安装失败并在 worker 控制台输出 `Uncaught (in promise) TypeError: Failed to execute 'addAll' on 'Cache': Request failed`，随后 DevTools › Application › Service workers 把该 worker 显示为 `#<id> is redundant`；其中 `Request failed` 正是 Chromium `cache.cc` 里的字符串。DevTools › Application › Storage › Cache storage 把每个缓存列为 `<name> - <origin>`，并逐条显示 **Response-Type**（`basic`、`cors`、`opaque`）与 **Content-Length** 列，这是找出哪些 opaque 条目在撑大配额的最快办法。
:::

## 示例

下面两个示例分别是版本化缓存模式的 install 与 activate 两半，之后是一个针对 `caches` 缺失环境的检测示例。

### 在 `install` 中原子性地预缓存应用壳

在 `event.waitUntil()` 内使用 `addAll()`，这样任何一个资源失败都会把有问题的 worker 挡在 active 槽位之外。

```js
const SHELL = 'shell-v3';
const ASSETS = ['/', '/app.js', '/app.css', '/offline.html'];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(SHELL).then((cache) => cache.addAll(ASSETS))
  );
});
```

由于 `addAll()` 是原子的，`shell-v3` 要么完整要么不存在；`/app.css` 返回 404 会让 Promise 被拒绝、安装失败，而原来处于 active 状态的 worker 继续服务。

### 在 `activate` 中删除被取代的缓存

旧缓存不会自动清理。新 worker 激活后，删除所有不等于当前名字的缓存。

```js
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((names) =>
      Promise.all(names.filter((n) => n !== SHELL).map((n) => caches.delete(n)))
    )
  );
});
```

这段代码要放在 `activate` 而不是 `install` 里：`install` 期间旧 worker 可能仍在用你要删除的那个缓存为页面提供服务。

### 检测支持，并在不支持时只在线运行

非安全源与没有该 API 的浏览器中没有 `caches`。下面的回退跳过 service worker 注册，并告知应用不要宣称支持离线。

```js
export function initOffline() {
  if ('caches' in window && 'serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js');
    return { offline: true };
  }
  // 没有 Cache Storage：只在线运行，并隐藏"可离线使用"标记。
  return { offline: false };
}
```

直接调用 `caches.open()` 的页面也应同样防护；在 `http:` 源上直接调用会在任何 Promise 产生之前就抛出。

## 另请参阅

- [Service Workers specification: Cache interface](https://w3c.github.io/ServiceWorker/#cache-interface)（w3c.github.io）
- [View Cache data in Chrome DevTools](https://developer.chrome.com/docs/devtools/storage/cache)（developer.chrome.com）
- [CacheStorage](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage)（developer.mozilla.org）
- [Service worker 缓存策略](/zh/reference/service-worker/caching-strategies/)
- [FetchEvent 与请求路由](/zh/reference/service-worker/fetch-event/)
- [Cache Storage](/zh/reference/storage/cache-storage/)
- [存储持久化、配额与驱逐](/zh/reference/storage/persistence/)