关键在于按请求意图精准分流、显式控制响应来源、严格匹配缓存键:需依request.destination分层处理document/script/image等类型,显式对齐credentials、查询参数与路径规范,并在activate阶段版本隔离清理旧缓存。

要让 Service Worker 在离线时提供完全定制化的缓存兜底,关键不是“拦截所有 fetch”,而是**按请求意图精准分流 + 显式控制响应来源 + 严格匹配缓存键**。手写 fetch 拦截时,必须绕过通用 match() 的陷阱,针对不同资源类型分别决策,同时确保缓存内容真正可用。
先区分 destination,再决定是否查缓存
不能对所有请求都调 caches.match(request)——它会把 POST、带 credentials 的登录请求也当成静态资源去匹配,结果返回空响应或错乱数据。必须依据 request.destination 做第一层判断:
- document:页面导航请求,必须有兜底。优先返回缓存中的 HTML;未命中则 fetch 并缓存响应体(注意 clone()),同时返回网络结果
- script / style / font / image:可安全缓存。直接 caches.match(request),命中即返回;未命中才 fetch 并 cache.put(request, response.clone())
- json / empty / other:默认不走缓存。直接 fetch(request),失败时再 fallback 到缓存(仅限非敏感 API)或返回自定义 offline.html
缓存匹配必须显式对齐请求特征
cache.match() 默认只比 URL 和 method,很多“查不到缓存”其实是匹配条件没对上:
- 若页面 fetch('/api/config', { credentials: 'include' }),缓存时就得用 caches.open('api-cache', { credentials: 'include' }),否则 match 失败
- 请求 URL 是 /logo.png?v=2.3.1,缓存里存的必须是 /logo.png?v=2.3.1,不能只存 /logo.png
- HTML 中引用 /app.js,install 阶段预缓存也得写 '/app.js'(绝对路径),不能写 'js/app.js'(相对路径会被解析为相对于 sw.js 所在目录)
离线兜底要分层设计,不能只靠一个 fallback 页面
所谓“完全定制化”,是指不同场景返回不同内容,而不是统一跳 offline.html:
- document 请求未命中缓存 → 返回预置的离线 HTML 字符串:
new Response('<h1>离线中</h1>', { headers: { 'Content-Type': 'text/html' } }) - script/style 请求未命中 → 返回 404 响应,避免执行中断:
new Response('', { status: 404 }) - 关键图片缺失 → 返回 base64 占位图或透明像素:
new Response(atob('R0lGODlhAQABAAAAACw='), { headers: { 'Content-Type': 'image/gif' } }) - API 请求失败且无缓存 → 返回业务约定的 mock 数据,比如 { code: -1, message: '网络不可用' }
版本隔离和清理要写进 activate 阶段
缓存名必须含当前版本标识(如 static-v2.4.0),并在 activate 中主动清理旧缓存:
- install 阶段只打开并填充新缓存,不删旧的
- activate 阶段先 self.clients.claim() 确保新 SW 接管所有页面,再遍历 caches.keys(),删除所有不含 'v2.4.0' 的缓存名
- 配合 self.skipWaiting() 缩短新旧 SW 共存时间,防止用户看到混合版本资源


















