第3章 Cache API 基础:缓存策略、命名与手动管理

Cache API 是 Service Worker 中最重要的存储接口之一,它允许开发者精确控制 HTTP 请求与响应的缓存行为。与浏览器自带的 HTTP 缓存不同,Cache API 完全由开发者编程管理,支持自定义策略、版本隔离和细粒度淘汰。本章将深入讲解三种核心缓存策略的实现原理、缓存命名规范以及如何手动控制缓存大小,避免存储溢出。

3.1 Cache API 核心操作

Cache API 基于 Request / Response 对进行存储,提供以下基础方法:

方法作用返回值
caches.open(cacheName)打开(或创建)一个命名缓存Promise<Cache>
cache.add(request)发起请求并将响应存入缓存Promise<void>
cache.put(request, response)手动将响应存入缓存Promise<void>
cache.match(request)从缓存中查找匹配的响应Promise<Response|undefined>
cache.delete(request)删除指定的缓存条目Promise<boolean>
caches.delete(cacheName)删除整个缓存对象Promise<boolean>
cache.keys()获取缓存中所有请求的列表Promise<Request[]>
关键区别:cache.add() 会自动发起请求并存入,要求响应状态码为 2xx;cache.put() 则完全由开发者控制,可存入任意响应(包括 opaque 响应)。

3.2 三种核心缓存策略

缓存策略决定了请求何时读取缓存、何时回退网络。以下三种策略是最基础的构建块,实际项目中常组合使用。

3.2.1 Cache First(缓存优先)

请求到达时优先从缓存读取,若未命中则发起网络请求并将结果写入缓存。适用于版本稳定的静态资源(CSS、JS、字体)。

self.addEventListener('fetch', (event) => {
  event.respondWith(cacheFirst(event.request));
});

async function cacheFirst(request) {
  const cache = await caches.open('v1-static');
  const cachedResponse = await cache.match(request);
  if (cachedResponse) {
    return cachedResponse;
  }
  try {
    const networkResponse = await fetch(request);
    if (networkResponse.ok) {
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  } catch (error) {
    return new Response('Network error', { status: 408 });
  }
}

3.2.2 Network First(网络优先)

先尝试从网络获取最新数据,网络失败时回退到缓存。适用于频繁更新但又需要离线兜底的内容(如新闻列表、API 数据)。

async function networkFirst(request) {
  const cache = await caches.open('v1-dynamic');
  try {
    const networkResponse = await fetch(request);
    if (networkResponse.ok) {
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  } catch (error) {
    const cachedResponse = await cache.match(request);
    if (cachedResponse) {
      return cachedResponse;
    }
    return new Response('Offline', { status: 503 });
  }
}

3.2.3 Stale-While-Revalidate(先缓存后更新)

立即返回缓存(即使过期),同时发起网络请求,在后台用最新响应更新缓存。适用于对实时性要求不高但希望下次请求最新的场景(如用户头像、文章列表)。

async function staleWhileRevalidate(request) {
  const cache = await caches.open('v1-stale');
  const cachedResponse = await cache.match(request);
  const fetchPromise = fetch(request).then((networkResponse) => {
    if (networkResponse.ok) {
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  }).catch(() => cachedResponse);
  return cachedResponse || fetchPromise;
}
策略选择速查:静态资源用 Cache First,用户数据用 Network First,可接受短暂滞后的内容用 Stale-While-Revalidate。

3.3 缓存命名与版本管理

正确的缓存命名是避免污染和实现平滑升级的基础。推荐使用 语义化版本 + 作用域 的命名模式:

  • v1::assets — 静态资源缓存
  • v1::pages — 页面 HTML 缓存
  • v1::api — API 响应缓存
  • v2::assets — 升级后的静态资源缓存

在 Service Worker 的 activate 事件中清理旧版本缓存:

const CACHE_VERSION = 'v2';
const CACHE_NAMES = [`${CACHE_VERSION}::assets`, `${CACHE_VERSION}::pages`];

self.addEventListener('activate', (event) => {
  event.waitUntil(cleanOldCaches(CACHE_NAMES));
});

async function cleanOldCaches(validNames) {
  const allNames = await caches.keys();
  const obsolete = allNames.filter(name => !validNames.includes(name));
  await Promise.all(obsolete.map(name => caches.delete(name)));
}
注意:不要在 install 事件中删除旧缓存。旧缓存应等到 activate 阶段再清理,以确保正在使用旧缓存的页面能够平滑过渡。

3.4 手动管理缓存大小

浏览器为每个源分配的存储空间有限(通常为总磁盘的 6%~10%),缓存无限制增长会导致存储溢出或被浏览器强制清除。以下方法实现手动大小控制。

3.4.1 限制条目数量(LRU 淘汰)

async function trimCacheByCount(cacheName, maxEntries) {
  const cache = await caches.open(cacheName);
  const requests = await cache.keys();
  if (requests.length > maxEntries) {
    const toDelete = requests.slice(0, requests.length - maxEntries);
    await Promise.all(toDelete.map(req => cache.delete(req)));
  }
}

3.4.2 限制缓存总大小(按响应体长度)

async function trimCacheBySize(cacheName, maxBytes) {
  const cache = await caches.open(cacheName);
  const requests = await cache.keys();
  let totalSize = 0;
  const entries = [];
  for (const req of requests) {
    const res = await cache.match(req);
    const size = res ? (await res.clone().arrayBuffer()).byteLength : 0;
    entries.push({ req, size });
    totalSize += size;
  }
  if (totalSize > maxBytes) {
    entries.sort((a, b) => a.size - b.size);
    while (totalSize > maxBytes && entries.length > 0) {
      const entry = entries.shift();
      await cache.delete(entry.req);
      totalSize -= entry.size;
    }
  }
}

3.4.3 结合 Storage API 获取配额

async function getStorageQuota() {
  if ('storage' in navigator && 'estimate' in navigator.storage) {
    const estimate = await navigator.storage.estimate();
    const usageMB = (estimate.usage / (1024 * 1024)).toFixed(1);
    const quotaMB = (estimate.quota / (1024 * 1024)).toFixed(1);
    console.log(`已使用: ${usageMB} MB / 配额: ${quotaMB} MB`);
    return { usage: estimate.usage, quota: estimate.quota };
  }
  return null;
}

建议在每次写入缓存后调用大小管理函数,并配合 setInterval 定期执行清理。

3.5 三种策略决策流程

三大缓存策略决策流程对比 三大缓存策略决策流程 HTTP 请求到达 Cache First 缓存命中? 返回缓存响应 否 请求网络并写入缓存 Stale-While-Revalidate 立即返回缓存(可过期) 后台发起网络请求 更新缓存(下次生效) Network First 请求网络 成功:返回并写入缓存 失败 回退到缓存 返回响应 适用场景:Cache First → 静态资源;Stale-While-Revalidate → 可接受滞后数据;Network First → 动态内容需离线兜底

3.6 调试方法

  1. Chrome DevTools → Application → Cache Storage:查看所有缓存对象及条目,支持手动删除。
  2. Console 快捷命令:
// 列出所有缓存名称
await caches.keys();

// 查看指定缓存中的请求列表
const cache = await caches.open('v1::assets');
const requests = await cache.keys();
requests.forEach(req => console.log(req.url));

// 删除过期缓存
await caches.delete('v0::assets');
  1. 模拟离线环境:在 DevTools Network 面板勾选 Offline,验证缓存策略是否按预期回退。
  2. 使用 Storage Panel 监控配额:在 DevTools 中查看 Storage 使用量,验证缓存大小管理是否生效。

3.7 部署步骤

  1. 在网站根目录创建 sw.js 文件,实现本章的缓存策略逻辑。
  2. 在主页面注册 Service Worker:
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered:', reg.scope))
    .catch(err => console.error('SW registration failed:', err));
}
  1. 构建时注入版本号:使用构建工具(Webpack/Vite)将 CACHE_VERSION 替换为当前构建哈希,确保每次部署自动更新缓存命名。
  2. 部署后在 DevTools 中验证:检查 Cache Storage 中是否出现新版本缓存,旧版本是否在 activate 后被清理。

3.8 常见错误处理

错误现象原因解决方案
缓存始终不更新install 后未调用 skipWaiting(),新 SW 处于 waiting 状态在 install 事件中执行 self.skipWaiting()
旧缓存残留activate 中未清理或命名不匹配使用白名单机制逐一比对并删除
存储配额溢出未做大小限制,缓存无限增长实现 trimCacheBySize 或 trimCacheByCount
opaque 响应缓存失败cache.add() 要求 2xx 状态码改用 cache.put() 手动存入 opaque 响应
跨域请求缓存失效未设置 no-cors 模式或服务端缺少 CORS 头在 fetch 中设置 {mode: 'no-cors'},并用 put 存入

3.9 性能与稳定性优化建议

  • 策略分级:对同一域名下的不同资源路径使用不同策略。例如 /static/* 用 Cache First,/api/* 用 Network First。
  • 缓存预热:在 install 事件中预缓存关键资源(App Shell),减少首次加载的网络依赖。
  • 写入节流:频繁更新的接口(如轮询)配合 Stale-While-Revalidate 时,限制缓存写入频率,避免无效 I/O。
  • 定期巡检:每周使用 navigator.storage.estimate() 记录存储使用量,设置告警阈值。
  • 失败降级:所有网络请求必须包含 catch 分支,至少返回一个备选响应(如离线占位图)。
  • 命名约定团队化:在项目中统一缓存命名前缀,例如 <app-name>::<version>::<type>,避免多人协作时命名冲突。
生产建议:使用 workbox 库可以免去手写缓存策略的繁琐工作,但理解底层 Cache API 原理有助于在出现问题时快速定位根因。建议新手先手写实现再切换到 workbox。