# CacheStorage 与 caches 全局对象

> caches 全局对象如何打开、列出和删除 Cache 对象，不透明响应的配额填充，以及写入失败时的 TypeError 与 QuotaExceededError。

`CacheStorage` 以 `caches` 全局对象的形式暴露在窗口、worker 和 Service Worker 中，是该源下具名 `Cache` 对象的注册表；每个 `Cache` 是一组 `Request`/`Response` 对，Service Worker 可以在离线时用它们应答 `fetch` 事件。它与 HTTP 缓存相互独立：数据只能通过脚本进出，HTTP 新鲜度头部一概不生效。

## 语法

```js
caches.open(cacheName)
caches.has(cacheName)
caches.delete(cacheName)
caches.keys()
caches.match(request)
caches.match(request, options)
```

每个方法都返回 Promise。`open()` 在具名缓存不存在时创建它，所以在名称里升级版本号（`shell-v4`）就足以开启一个全新的存储。`CacheStorage` 上的 `match()` 按创建顺序搜索全部缓存，返回第一个命中或 `undefined`。主线程自 Chrome 43、Firefox 41、Safari 11.1 起可用（BCD `api.CacheStorage`），Service Worker 内自 Chrome 40 起可用。

## 参数

| 方法 | 参数 | 类型 | 含义 |
|---|---|---|---|
| `open`、`has`、`delete` | `cacheName` | string | 缓存名称。区分大小写，作用域为源；同一源上的两个 Service Worker 共享同一命名空间。 |
| `match` | `request` | `Request` 或 string | 要查找的请求。字符串会按当前基准 URL 转成 `Request`。 |
| `match` | `options.ignoreSearch` | boolean | 比较 URL 时忽略查询串。默认 `false`。 |
| `match` | `options.ignoreMethod` | boolean | 允许匹配非 `GET` 请求。默认 `false`，因此 `POST` 不会匹配。 |
| `match` | `options.ignoreVary` | boolean | 忽略已存响应的 `Vary` 头。默认 `false`。 |
| `match` | `options.cacheName` | string | 只搜索指定缓存。Chromium 的 `CacheStorage.match()` 只支持 `ignoreSearch` 和 `cacheName`（BCD `api.CacheStorage.match`）。 |

写入方法在 `Cache` 上而不在 `CacheStorage` 上：`cache.put(request, response)` 存入你已经持有的一对对象，`cache.add(request)` 与 `cache.addAll(requests)` 先抓取再存入，`cache.delete(request)` 删除一条。

## 异常

| 异常 | 位置 | 触发条件 |
|---|---|---|
| `TypeError` | `cache.put()` | 请求 URL 的 scheme 不是 `http:` 或 `https:`，响应状态为 `206 Partial Content`，或响应带 `Vary: *`。Promise 被拒绝。 |
| `TypeError` | `cache.add()`、`cache.addAll()` | 抓取到的响应不是 `ok`（状态码不在 200 到 299 之间）。不透明响应状态为 `0`，因此一律落入此列；要有意存入不透明响应，改用 `fetch()` 加 `put()`。 |
| `QuotaExceededError` `DOMException` | 任何写入 | 该源配额已用尽。Promise 被拒绝，已存条目不受影响。 |
| `SecurityError` `DOMException` | 任何 `caches` 方法 | 源是不透明源（没有 `allow-same-origin` 的沙箱 `<iframe>`），或该站点的存储被禁用。 |

Cache Storage 不会让条目过期。以 `Cache-Control: max-age=60` 存入的响应一年后原样返回；新鲜度是 Service Worker 的职责。

## 浏览器支持

`CacheStorage` 与 `Cache` 自 Chrome 43、Firefox 41、Safari 11.1 起在所有引擎中可用（BCD `api.CacheStorage`、`api.Cache`），窗口、专用 worker 和 Service Worker 一视同仁，且仅限安全源。`cache.add()` 自 Chrome 46 起要求 HTTPS。唯一缺失该 API 的场景是禁用了站点存储的 WebView 或浏览器，此时每个方法都以 `SecurityError` 拒绝。

## 配额计算

Cache Storage 与 IndexedDB、OPFS 共用该源的单一配额，数值由 [`navigator.storage.estimate()`](/zh/reference/storage/quota-estimate/) 报告。不透明响应按填充后的体积计费，避免页面借配额 API 测出跨源资源大小：在 Chromium 中每条不透明响应无论实际多大都计入约 7 MB 用量（[Understanding storage quota](https://developer.chrome.com/docs/workbox/understanding-storage-quota)，developer.chrome.com）。跨源资源用 `cors` 模式请求，或在加载它的标签上加 `crossorigin`，响应就不再是不透明的，按真实大小计费。

## 示例

三个示例分别覆盖安装期预缓存、由 `fetch` 填充的运行时缓存，以及页面接触 `caches` 之前需要的特性检测。

### 带版本的预缓存与 activate 时清理

缓存名携带版本号。`install` 填充新缓存；`activate` 删除所有名称不同的缓存，于是上一次部署的文件被回收而不是堆积。

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

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

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

`addAll()` 是原子的：四个请求里有一个返回 404，整个调用就以 `TypeError` 拒绝，`install` 失败。对应用壳而言这正是你想要的行为。调用 `caches.match(event.request)` 的 `fetch` 处理函数会继续从旧缓存应答，直到新 worker 激活。

### 在一个处理函数里存入并返回响应

`Response` 的 body 只能读一次。同一响应还要交给页面时，先克隆再 `put()`；不要在 `respondWith()` 里等待 `put()` 完成，那会让页面为一次磁盘写入而等待。

```js
self.addEventListener('fetch', (event) => {
  if (event.request.method !== 'GET') return;
  event.respondWith(
    caches.match(event.request).then((hit) => {
      if (hit) return hit;
      return fetch(event.request).then((response) => {
        if (response.ok) {
          const copy = response.clone();
          event.waitUntil(caches.open('runtime-v1').then((cache) => cache.put(event.request, copy)));
        }
        return response;
      });
    })
  );
});
```

`response.ok` 守卫把 404 和不透明响应挡在缓存之外；没有它，离线用户会把昨天的错误页当作命中拿到。

### 检测 API 并退回网络

不安全源上没有 `caches`，早于「语法」一节所列版本的浏览器也没有。运行在页面里（Service Worker 之外）的代码应先检测再使用。

```js
async function cachedOrNetwork(url) {
  if (!('caches' in self)) {
    return fetch(url); // 没有 Cache Storage：纯网络，没有离线副本
  }
  const hit = await caches.match(url);
  return hit ?? fetch(url);
}
```

同一个函数在 worker 和窗口里都能用，因为两者都暴露 `self`。

:::observed
Chrome DevTools 的 Application > Storage > Cache storage 在源下列出每个具名缓存，选中后显示条目表，列为 **Name**、**Response-Type**、**Content-Type**、**Content-Length**、**Time Cached**、**Vary Header**；不透明条目的 **Response-Type** 为 `opaque`、**Content-Length** 为 `0`，而 Storage 用量条却增加了数 MB，这就是上文所说的填充（[Debug Progressive Web Apps](https://developer.chrome.com/docs/devtools/progressive-web-apps)，developer.chrome.com）。
:::

## 另请参阅

- [Service Workers: CacheStorage interface](https://w3c.github.io/ServiceWorker/#cachestorage-interface)（w3.org）
- [Understanding storage quota](https://developer.chrome.com/docs/workbox/understanding-storage-quota)（developer.chrome.com）
- [Cache API](/zh/reference/service-worker/cache-api/)
- [缓存策略](/zh/reference/service-worker/caching-strategies/)
- [StorageManager.estimate()](/zh/reference/storage/quota-estimate/)
- [逐出与尽力而为存储](/zh/reference/storage/eviction/)