关键在于用service worker按资源类型精细分流:html缓存优先+后台更新,css/js等缓存优先+静默更新,api网络优先+缓存兜底;配合语义化缓存名、自动清理、精准匹配与离线兜底。

要让 Web 应用在弱网甚至完全离线时仍能稳定运行,关键不是“把所有东西都塞进缓存”,而是用 Service Worker 的 fetch 拦截机制,按资源类型、业务重要性和更新频率做精细分流——企业级策略的核心在于“可控”和“可维护”,而不是一味求全。
分层缓存:按资源类型定策略
不同资源对一致性和加载速度的要求差异极大,不能一锅煮。应在 fetch 事件中依据 request.destination 或 URL 特征精准分流:
-
HTML 页面(
destination === 'document'):采用「缓存优先 + 网络后台更新」。首次返回缓存内容避免白屏,同时 fetch 新版本并写入缓存,下次访问即生效;若缓存为空,则直接网络请求,不降级到错误页。 -
CSS / JS / 字体 / 图片(
destination为style、script、font、image):走「缓存优先 + 静默更新」。命中即返回,未命中再 fetch 并cache.put(request, response.clone());注意克隆响应,避免 body 被读取两次。 -
API 接口(URL 匹配
/api/或mode === 'cors'):默认「网络优先 + 缓存兜底」。先 fetch,失败时再查缓存并返回;对非幂等请求(如 POST)跳过缓存逻辑,避免误存敏感响应。
缓存命名与版本控制
企业项目迭代频繁,旧缓存不清理会占用空间、导致资源错乱。必须引入语义化缓存名和自动清理机制:
- 缓存名建议包含应用名、环境(如
prod)、版本号或构建哈希,例如myapp-static-v2.3.1或myapp-api-20260415;避免硬编码v1、v2这类无意义数字。 - 在
activate事件中,只保留当前版本缓存,其余全部删除:caches.keys().then(keys => Promise.all(keys.filter(k => !k.includes(currentCacheName)).map(k => caches.delete(k))))。 - install 阶段调用
self.skipWaiting(),activate 阶段调用self.clients.claim(),确保新 SW 尽快接管页面,减少多版本共存窗口期。
匹配精度与常见陷阱
很多缓存失效不是代码逻辑错,而是匹配条件没对上。需特别注意以下细节:
- 预缓存的 URL 必须与页面实际发起的请求 URL 完全一致:包括协议、域名(localhost 可省略)、路径前缀、尾部斜杠、查询参数(如
logo.png?v=3)。相对路径在cache.addAll()中会被解析为相对于 SW 文件位置,极易出错,推荐统一用绝对路径。 -
cache.match()默认忽略请求头和 credentials。若页面请求带credentials: 'include',缓存时也需用相同选项打开 cache,否则匹配失败。可在 open 时传参:caches.open(cacheName, { ignoreSearch: false })控制是否忽略 query string。 - scope 必须覆盖待拦截资源路径。注册时显式指定
{ scope: '/' },尤其当sw.js不在根目录时;否则它只能拦截同级及子目录下的请求,比如/js/sw.js默认无法拦截/index.html。
离线体验兜底与监控
真正的企业级策略必须考虑“缓存全部失效”的极端情况,并具备可观测性:
- 为关键路由(如首页、登录页)预置一个轻量级离线页(
offline.html),在 fetch 中检测网络状态或捕获全局 fetch 失败后,返回该页面而非空白或报错。 - 在
fetch响应逻辑中加入简单日志(如console.debug('[SW] Cache hit:', request.url)),配合 Chrome DevTools 的 Application → Service Workers 面板,可实时查看激活状态、缓存内容、触发的事件。 - 对高价值静态资源(如核心 JS bundle),可在 install 阶段用
cache.addAll()强制预载;对低频或大体积资源(如用户上传图片),改用「按需缓存 + TTL 过期」策略,避免初始包过大。










