Service Worker 无法实现真正的版本镜像,需通过语义化缓存命名、精准URL匹配、install/activate分阶段操作及skipWaiting/clients.claim控制激活时机来模拟。
不能靠 fetch 拦截实现真正的“版本镜像”——Service Worker 本身不提供资源快照式版本隔离能力,它只做缓存键匹配;所谓“镜像”,实际是通过缓存命名 + 精准 URL 匹配 + 请求参数一致性来模拟的。
Cache API 不支持同一缓存名下的原子替换,caches.open('static') 打开的是一个可变引用。若新旧版本共用同一个缓存名,cache.put() 会覆盖已有条目,但旧页面仍在使用该缓存名,极易导致混用。
static-v2.3.1 或 static-20260414
caches.delete('static-v2.2.0'))哪怕一个 trailing slash、一个查询参数、一个大小写差异,caches.match(request) 就会失败——这不是 bug,是 Cache API 的精确匹配设计。
<script src="/js/app.js?v=2.3.1"></script>,预缓存数组里就必须写 '/js/app.js?v=2.3.1'
'js/app.js' 会被解析为相对于 sw.js 文件位置,而非站点根目录manifest.json)被读入 SW,并原样用于 cache.addAll()
request.url 在 fetch 事件中是完整绝对 URL,匹配时别漏掉协议和域名(开发时常见 localhost vs 127.0.0.1 不一致)request.destination 在某些场景下不可靠:比如用 fetch('/api/config.json') 加了 {mode: 'no-cors'},destination 可能是 'empty';又或者字体文件在 Firefox 中可能返回 'font',而 Chrome 是 'style'。
/.(js|css|png|jpg|woff2?|svg)$/i.test(request.url)
request.destination === 'document' 是安全的credentials: 'include'),否则 cache.match() 因 credential mismatch 失败,除非你显式用 { ignoreSearch: false, ignoreVary: true } 控制匹配行为Access-Control-Allow-Origin: *),否则 cache.put() 会静默失败默认情况下,新注册的 Service Worker 会卡在 waiting 状态,直到所有受控页面关闭再激活。这意味着用户刷新页面时,仍由旧 SW 响应请求,旧缓存继续生效,“版本镜像”根本不会切换。
self.skipWaiting(),让新 SW 跳过 waiting 直接进入 activatingself.clients.claim(),立即接管所有已打开的 clients(包括当前页面)clients.claim() 不会影响尚未加载完成的资源请求,那些仍走旧逻辑;所以关键资源最好在 HTML 中内联或预加载,减少首屏依赖 SW 的窗口期真正难的不是写对 fetch 逻辑,而是让每个资源请求的 URL、缓存键、缓存名、激活时机四者严丝合缝——差一个字符或一毫秒,镜像就碎了。