service worker 的 fetch 事件需按资源类型分流代理:html 离线优先立即返回,css/js 缓存即用再更新,非敏感 api 缓存兜底设 ttl,post 等非幂等请求直连网络;url、credentials、缓存名须规范,未匹配请求兜底 offline.html 或 mock 响应。

要让页面在弱网或断网时仍能快速加载、不白屏、不报错,核心不是“把所有请求都缓存”,而是用 Service Worker 的 fetch 事件做有判断的异步代理——对关键资源走“离线优先”,对动态数据保留灵活性,同时避免缓存污染和匹配失效。
明确离线优先的适用范围
离线优先不是全盘缓存,而是聚焦于用户可感知的核心体验资源:
- HTML 页面:必须缓存,且首次访问就应返回(哪怕旧版),避免白屏或连接错误页
- CSS / JS / 字体 / 关键图片:缓存后立即使用,再后台静默更新,保障样式和交互不中断
- 非敏感 API(如配置、菜单、静态列表):可缓存兜底,但需设短 TTL 或配合版本标识,避免 stale 数据误导用户
- 跳过缓存的请求:POST/PUT/DELETE 等非幂等操作、带 token 的用户数据接口、实时消息轮询——一律直连网络,不参与离线代理逻辑
fetch 事件中实现异步代理的关键写法
不能简单 return fetch(request) 或只查缓存,要按类型分流,并用 Promise 链控制响应时机:
- 对
request.destination === 'document':先caches.match(),命中则立刻返回;未命中则fetch()并cache.put(),但不阻塞响应——用户看到的是网络结果,缓存为下次服务 - 对
script/style:直接caches.match(request).then(r => r || fetch(request).then(res => { cache.put(...); return res; })),确保资源不因网络抖动延迟执行 - 对匹配
/api/的请求:走“网络优先 + 缓存兜底”模式,fetch().catch(() => caches.match(request)),失败时返回缓存内容而非空响应
确保缓存真正可用的三个实操细节
很多“离线失败”问题其实出在缓存本身不可用,而非策略逻辑:
-
URL 必须完全一致:页面请求
/index.html,缓存时就得写index.html或/index.html(带斜杠),不能省略或加参数;若 HTML 中引用app.js?v=2.1,缓存列表里也得带这个完整字符串 -
credentials 匹配要显式处理:如果请求带
credentials: 'include'(如登录态接口),缓存时需用caches.open('name', { credentials: 'include' }),否则match()会因凭据不匹配而返回undefined -
缓存名需语义化并定期清理:用
v1-core、v1-api-cache这类命名,激活阶段调用self.addEventListener('activate', e => { e.waitUntil(caches.keys().then(keys => Promise.all(keys.map(k => k.startsWith('v1-') ? null : caches.delete(k))))) }),避免旧缓存堆积
补充离线兜底体验
即使策略再细,总有缓存未覆盖的请求。可在 fetch 事件末尾加一层保底:
- 对未匹配任何规则的 GET 请求,尝试
caches.match('/offline.html')返回定制离线页 - 对 API 类请求兜底失败,返回一个结构一致的 mock 响应(如
new Response(JSON.stringify({ code: 0, data: [] }), { headers: { 'Content-Type': 'application/json' } })),避免前端 JSON 解析报错 - 不建议全局 fallback 到
'Offline'文本,用户需要的是可操作界面,不是提示语










