必须clone响应体,因Response流单次消费;未clone会导致缓存失败或页面解析异常;正确做法是fetch后立即clone,一份返回页面、一份存入缓存。

直接用 CacheStorage 缓存 Response 克隆体,是实现首屏秒开和离线可用最可靠的方式之一。关键不在“能不能存”,而在于“怎么存得对、取得准、更新得稳”——克隆只是技术动作,背后是资源类型判断、缓存生命周期设计和请求匹配逻辑的协同。
为什么必须 clone() 响应体?
Response 对象底层是一个只读、单次消费的流。一旦被读取(比如调用 response.json() 或传给 cache.put()),就无法再次读取。若不 clone,会出现以下典型错误:
- 浏览器渲染 HTML 时消耗了响应流,后续写入缓存失败,报错
TypeError: body stream already read - 缓存写入成功,但页面解析失败,因为原始响应已被提前读取
- 多个地方(如日志、鉴权、缓存)同时需要响应内容,未 clone 就无法复用
正确做法:在 fetch 到响应后、任何读取操作前,立即调用 response.clone(),一份用于返回页面,一份用于存入缓存。
哪些接口适合用 Response 克隆缓存?
不是所有 API 都适合走 CacheStorage。优先选择满足以下全部条件的 GET 请求:
- 首屏强依赖:如
/api/v1/banner、/api/v1/menu、/api/v1/categories - 低频变动:配置类、字典类、静态内容类,更新周期以天或周计
- 无用户身份绑定:URL 不含
token、uid等动态参数;不依赖 Cookie 或 Authorization 头 - 响应体积适中:单条 ≤ 7MB(避免因过大导致写入失败或触发浏览器限制)
含余额、未读数、实时库存等敏感或高时效字段的接口,不应进 CacheStorage,可改用 localStorage + 时间戳做轻量兜底。
如何在 Service Worker 中安全写入克隆体?
写入必须发生在 fetch 事件中,并严格遵循“先 clone、再分发、后缓存”的顺序:
- 用正则或 pathname 判断是否为目标接口(如
/api/v1/(banner|menu)/) - fetch 网络得到 response 后,立刻
const cached = response.clone() - 将
cached交给cache.put(request, cached)存储 - 将原始
response直接 return 给页面,确保流未被二次读取
示例片段:
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (/^\/api\/v1\/(banner|menu)\//.test(url.pathname)) {
event.respondWith(
(async () => {
const cache = await caches.open('api-v2');
const cached = await cache.match(event.request);
if (cached) return cached;
const response = await fetch(event.request);
const cloned = response.clone();
cache.put(event.request, cloned); // 写入克隆体
return response; // 返回原始响应
})()
);
}
});
如何避免缓存键不一致导致克隆白存?
缓存写入成功,但后续 caches.match() 找不到,90% 是 URL 不一致。常见陷阱:
- 页面请求
/style.css?v=2.1.0,但预缓存时只存了/style.css - 大小写混用:
/API/V1/BANNER和/api/v1/banner被视为不同键 - 尾部斜杠差异:
/menuvs/menu/
解决方法:
- 预缓存阶段:确保
cache.addAll([...])的路径与 HTML 中实际引用的 URL 完全一致(推荐用 Webpack 输出哈希文件名) - 运行时匹配:对带查询参数的资源,使用
caches.match(request, { ignoreSearch: true }) - 调试时打开 DevTools → Application → Cache Storage,展开查看真实缓存 key 格式

















