# 预缓存策略

> 预缓存如何在安装时把构建期的外壳资源列表填入 Cache Storage、为何每个条目需要修订号或带哈希的 URL，以及 Workbox 清单如何处理这件事。

预缓存在 Service Worker 的 `install` 事件中、在任何页面请求之前，把一份构建时固定的文件列表存入 Cache Storage，于是应用外壳从首次访问起就在设备上，并且可以离线加载。它是运行时缓存的对应物，后者在响应被请求时才存储；两者配合使用：预缓存外壳，运行时缓存内容。

## 工作原理

安装处理函数用这份列表调用 `cache.addAll()`；任一条目失败则 Promise 拒绝、worker 不安装，这避免了半成品外壳上线。之后的每次部署都发布新列表。难点在于变更检测：URL 不变的文件（`/index.html`）即使内容不同，在缓存看来也一模一样，所以每个条目要么在 URL 里带内容哈希（`/app.3f2a1c.js`），让改动的文件成为新 URL，要么带一个独立的修订字符串供 worker 在安装时比较（[workbox-precaching](https://developer.chrome.com/docs/workbox/modules/workbox-precaching)，developer.chrome.com）。手工维护修订号是最容易出错的一步，所以通常由构建插件生成清单。

### 什么属于预缓存

外壳：渲染框架的 HTML、它的 CSS 和 JavaScript 分块、图标和离线页。按用户或按请求变化的内容、大体积媒体，以及用户可能永远不会打开的东西，属于某种策略之下的运行时缓存（[caching strategies overview](https://developer.chrome.com/docs/workbox/caching-strategies-overview)，developer.chrome.com）。膨胀到数兆字节的预缓存会拖慢每次安装，在 Chrome 上还会因为 `addAll()` 争抢带宽而推迟首次访问自身的请求；典型应用的外壳只有几百 KB。

### 与 HTTP 缓存的相互作用

安装期对每个条目的 `fetch()` 经过 HTTP 缓存。URL 不变却带着长 `max-age` 的文件，可能在修订号已变的情况下仍以过期副本进入预缓存；服务端规则是只给带哈希的 URL 设长 `max-age`，对 HTML 和 Service Worker 脚本本身这类无哈希的 URL 设 `no-cache`（需重新验证）（[Service worker caching and HTTP caching](https://web.dev/articles/service-worker-caching-and-http-caching)，web.dev）。Workbox 把修订号作为 `__WB_REVISION__` 查询参数附加到无哈希的条目上，以 `cache: 'reload'` 的语义绕过过期的 HTTP 缓存条目，而页面请求的 URL 不变。

### 支持位置

预缓存只用到 `caches.open()`、`cache.addAll()` 以及 `install` 与 `activate` 事件，它们存在于 Chrome 40、Firefox 44、Safari 11.1 和所有现行引擎（BCD `api.CacheStorage`、`api.ServiceWorkerGlobalScope`）。Safari 对脚本可写存储的七天逐出会删除用户未交互站点的预缓存，之后的下一次访问会重新安装它；见[逐出与尽力而为存储](/zh/reference/storage/eviction/)。

## 示例

Workbox 形式是多数项目实际发布的；手写形式展示它在做什么。

### 使用生成清单的 Workbox precacheAndRoute

构建工具把 `self.__WB_MANIFEST` 替换为 `{ url, revision }` 列表；带哈希的 URL 其 `revision` 为 `null`，其他为内容哈希。`precacheAndRoute()` 负责安装、提供和清理。

```js
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';

cleanupOutdatedCaches();
precacheAndRoute(self.__WB_MANIFEST);
// 构建时生成，例如：
// [{ url: '/index.html', revision: '383676' }, { url: '/app.3f2a1c.js', revision: null }]
```

`cleanupOutdatedCaches()` 移除旧版 Workbox 遗留的预缓存；当前预缓存自身的过期条目在 `activate` 期间移除，不需要它。

### 带修订号的手写预缓存

不用 Workbox 时，同一机制就是一张 URL 到修订号的映射，加上只重新抓取变化条目的安装处理函数，第二次安装就不必重新下载整个外壳。

```js
const PRECACHE = 'precache-v2';
const MANIFEST = { '/index.html': 'a1b2c3', '/app.3f2a1c.js': null, '/offline.html': 'd4e5f6' };

self.addEventListener('install', (event) => {
  event.waitUntil((async () => {
    const cache = await caches.open(PRECACHE);
    for (const [url, revision] of Object.entries(MANIFEST)) {
      const key = revision ? `${url}?__rev=${revision}` : url;
      if (!(await cache.match(key))) {
        await cache.put(key, await fetch(key, { cache: 'reload' }));
      }
    }
  })());
});
```

`fetch` 处理函数对请求的 `url` 查找 `${url}?__rev=${revision}`；`{ cache: 'reload' }` 选项强制变化的条目绕过 HTTP 缓存走网络。

### 在依赖预缓存之前检测可用的 Cache Storage

页面可以判断外壳是否已预缓存，否则保持网络优先的行为，例如用来决定是否显示「可离线使用」的指示。

```js
async function shellIsPrecached() {
  if (!('caches' in window) || !('serviceWorker' in navigator)) {
    return false; // 没有 Cache Storage：什么都没有预缓存
  }
  const names = await caches.keys();
  const precache = names.find((n) => n.startsWith('workbox-precache') || n.startsWith('precache-'));
  if (!precache) return false;
  const cache = await caches.open(precache);
  return Boolean(await cache.match('/index.html', { ignoreSearch: true }));
}
```

`ignoreSearch: true` 很重要，因为存储的键带着修订号查询参数；没有它，对 `/index.html` 的查找会落空。

:::observed
Workbox 的预缓存是以 `workbox-core` 中 `cacheNames.precache` 命名的缓存，默认为 `workbox-precache-v2-` 加注册作用域，所以 Chrome DevTools 的 Application > Storage > Cache storage 会显示诸如 `workbox-precache-v2-https://example.com/` 的条目，其中无哈希文件的行在 **Name** 列带有 `?__WB_REVISION__=<哈希>` 后缀，带哈希的文件则以原始 URL 存储（[workbox-core](https://developer.chrome.com/docs/workbox/modules/workbox-core)，developer.chrome.com）。一次改动了 `index.html` 的部署之后，该行的后缀改变，新 worker 激活后旧行消失。
:::

## 另请参阅

- [workbox-precaching](https://developer.chrome.com/docs/workbox/modules/workbox-precaching)（developer.chrome.com）
- [Service worker caching and HTTP caching](https://web.dev/articles/service-worker-caching-and-http-caching)（web.dev）
- [应用外壳（app shell）模型](/zh/reference/performance/app-shell/)
- [CacheStorage 与 caches 全局对象](/zh/reference/storage/cache-storage/)
- [Workbox](/zh/reference/service-worker/workbox/)
- [逐出与尽力而为存储](/zh/reference/storage/eviction/)