# Cache Storage：离线 PWA 背后的请求/响应存储

> Cache Storage API 如何为支持离线的 PWA 存储 Request/Response 对——CacheStorage 与 Cache 接口、它在哪里运行、它与 HTTP 缓存有何不同，以及版本管理与清理的实践规则。

**一句话：** Cache Storage 是面向 `Request`/`Response` **对**、按源限定的可编程存储——service
worker 用它在离线时提供资源与 API 响应。与浏览器自动管理的 HTTP 缓存不同，由你显式决定它
保存什么。

## Cache Storage 对比 HTTP 缓存与 IndexedDB

- **HTTP 缓存**由浏览器自动管理，且与 Cache Storage 相互独立；Cache API 不遵循 HTTP 缓存
  响应头。你无法直接读写 HTTP 缓存。
- **Cache Storage** 是一个你直接掌控的存储：你显式地 `put`、`match`、`delete` 整个 `Response`
  对象，以 `Request` 为键。它让 service worker 的 `fetch` 处理器能够在无网络时应答请求。
- **IndexedDB** 存储**结构化记录与 blob**，而非 HTTP 响应。应用数据（草稿、排队的变更）用
  IndexedDB；应用外壳与已取资源用 Cache Storage。

## 两个接口

- **`CacheStorage`** —— 在窗口、Worker 与 service worker 中以 `caches` 暴露。它是命名 `Cache`
  对象的注册表：`caches.open(name)`、`caches.has(name)`、`caches.delete(name)`、
  `caches.keys()`，以及在所有缓存中跨库查找的便捷方法 `caches.match()`。
- **`Cache`** —— 单个命名的请求→响应条目存储。关键方法：
  - `cache.put(request, response)` —— 存入你已有的一对。
  - `cache.add(request)` / `cache.addAll(requests)` —— 一步取回并存储。
  - `cache.match(request)` / `cache.matchAll()` —— 查找已存储的响应。
  - `cache.delete(request)` —— 移除一个条目。

`Cache` 存储 `Response` 对象。由于响应体只能被读取一次，当你还想把响应返回给页面时，请存储
一个**克隆**（`response.clone()`）。

## 它在哪里运行、用来做什么

`caches` 在窗口、Web Worker 中可用，并且——关键在于——在 service worker 中可用，这正是让
service worker 的 `fetch` 事件能在网络不可用时从缓存应答的原因。在 `install` 期间预存应用
外壳，然后在 `fetch` 中按你选定的策略从缓存提供，并回退到（或从）网络更新。

```js
const CACHE = 'shell-v3';
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((n) => n !== CACHE).map((n) => caches.delete(n)))
    )
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((hit) => hit || fetch(event.request))
  );
});
```

## 版本管理与清理

在正常使用下，Cache Storage 不会自行移除你的条目——你放入的内容会一直存在，直到你删除它。
这使得一套刻意的版本管理方案不可或缺：

- **给缓存名加上版本**（`shell-v3`）。资源变化时提升名称。
- **在 `activate` 中删除被取代的缓存**（如上），让上一版本的文件被回收，而非不断累积。
- **谨慎对待不透明响应。** 跨源 `no-cors` 请求会产生状态为 `0` 的不透明响应，它只能用
  `put` 存储；仅在必须时才缓存它们。

## 配额与持久化

Cache Storage 的数据计入由 Storage API 管理的源存储用量，与 IndexedDB 等其他 web 存储一起
计算。当达到配额上限时，浏览器可能逐出某个源的存储，因此把缓存的资源当作**可丢弃的**，并
随时准备重新获取。在数据必须经受逐出之处，用 `navigator.storage.persist()` 申请持久存储。
（完整的配额与逐出模型见存储持久化参考。）

## 浏览器与生态支持

Cache Storage API（`CacheStorage` 与 `Cache`）在当前主流浏览器上获得支持，并且可从 service
worker 访问，使其成为 PWA 离线资源的基线存储。

## 实践清单

- [ ] 请求/响应类资源用 Cache Storage；结构化应用数据用 IndexedDB。
- [ ] 若还要把响应返回给页面，缓存前先 `clone()`。
- [ ] 给缓存名加版本，资源变化时提升名称。
- [ ] 在 service worker 的 `activate` 事件中删除旧缓存。
- [ ] 把缓存视为可丢弃；为重新获取做好设计，数据必须经受逐出时调用 `persist()`。
- [ ] 谨慎缓存不透明的跨源响应——你无法检查它们。