ServiceWorker不直接支持版本镜像,需通过缓存命名带语义版本号、预缓存URL与请求完全一致、用正则而非destination判断静态资源、调用skipWaiting和clients.claim实现。关键在于缓存键、缓存名、URL与激活时机的精确对齐,否则策略失效。
先说一句大实话:指望 Service Worker 的 fetch 拦截机制能直接实现“版本镜像”,从一开始就走错了方向。Service Worker 本质上不提供资源快照式的版本隔离能力,它只干一件事——缓存键匹配。真正意义上的“镜像”,是通过缓存命名、精准 URL 匹配、请求参数一致性这三者模拟出来的效果。这事儿最关键的地方在于:你必须把每一个环节都对齐,但凡有一处脱节,整个策略就会碎掉。
Cache API 有个很明显的局限:它不支持对同一个缓存名做原子替换。caches.open('static') 打开的只是一个可变引用,新旧版本一旦共用同一个缓存名,cache.put() 就会覆盖已有条目。但麻烦在于,旧页面还在使用该缓存名,覆盖操作很容易导致版本混用——用户可能拿到一部分新资源配上另一部分旧资源。
长期稳定更新的攒劲资源: >>>点此立即查看<<<
正确的做法是:缓存名里明确带上版本标识,比如 static-v2.3.1 或 static-20260414。install 阶段只往新缓存里写入数据,不去动旧缓存。activate 阶段再做一遍批量清理——把非当前版本的缓存挨个删掉,像 caches.delete('static-v2.2.0') 这样操作。另外要避免用时间戳以外的动态值做版本号,比如哈希值。构建产物没变的情况下也会触发重缓存,白白浪费带宽。
一个 trailing slash 的差异,一个查询参数的有无,甚至大小写不一致——caches.match(request) 都会直接失败。这算不上 bug,而是 Cache API 的精确匹配机制就是如此。
那么问题来了:HTML 里如果写了 ,预缓存数组里就必须同样写 '/js/app.jsv=2.3.1'。不要依赖相对路径,'js/app.js' 会被解析成相对于 sw.js 文件位置,而不是站点根目录——这在开发环境里很容易踩坑。如果用了 Webpack 或 Vite 构建,确保生成的资源清单(比如 manifest.json)能正确读入 Service Worker,原样传给 cache.addAll()。还有一个容易忽略的点:request.url 在 fetch 事件里是完整绝对 URL,做匹配时不要把协议和域名漏掉——开发时 localhost 和 127.0.0.1 不一致的情况非常常见。
request.destination 这个属性在某些场景下相当不可靠。举个例子:用 fetch('/api/config.json') 加了 {mode: 'no-cors'},destination 可能会返回 'empty';再比如字体文件在 Firefox 和 Chrome 下的行为也不一致——前者返回 'font',后者则可能是 'style'。靠 destination 做判断,很容易漏掉一部分资源。
更稳妥的方式是结合正则匹配 URL 路径:/\.(js|css|png|jpg|woff2|svg)$/i.test(request.url)。对于 HTML 页面单独处理,用 request.destination === 'document' 是安全的。还有一个需要警惕的坑:避免缓存带有 credentials 的请求——比如 credentials: 'include',否则 cache.match() 会因为凭据不匹配而失败,除非你显式地用 { ignoreSearch: false, ignoreVary: true } 控制匹配行为。如果资源来自 CDN,必须确保其 CORS 头允许缓存(Access-Control-Allow-Origin: *),否则 cache.put() 会静默失败,连个报错都没有。
默认情况下,新注册的 Service Worker 会卡在 waiting 状态,必须等到所有受控页面关闭才能激活。这意味着用户刷新页面时,仍然由旧 SW 响应请求,旧缓存继续生效——所谓“版本镜像”根本不存在,本质上就是原地踏步。
解决方法很简单:在 install 事件末尾调用 self.skipWaiting(),让新 SW 跳过 waiting 状态直接进入 activating;在 activate 事件里调用 self.clients.claim(),立即接管所有已打开的 clients,包括当前页面。这两步只在需要“本次刷新即生效”的场景下启用。生产环境如果担心灰度风险,可以改为按 query 参数或 header 控制是否执行。不过要注意:clients.claim() 不会影响尚未加载完成的资源请求,那些请求走的仍然是旧逻辑。所以关键资源最好在 HTML 中内联或预加载,减少首屏依赖 SW 的窗口期。
回过头来看,整件事最难的其实不是怎么写对 fetch 逻辑,而是如何让每个资源请求的 URL、缓存键、缓存名、激活时机这四者严丝合缝地对齐。差一个字符,或者差一毫秒的时机,“镜像”就会碎掉。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述