企业H5站点升级PWA (六)
如何在 Service Worker 中实现缓存失效策略,以确保用户始终获取到最新的资源?
下面基于你当前的 sw.js(CACHE_VERSION = 'v0.0.5' 那个),给一套简单好维护的缓存失效策略,让用户尽量总是拿到最新资源,同时仍然享受离线能力。
可以组合三层:
- 版本号 + 清理旧缓存(你已做,继续用)。
- 动态资源使用 stale-while-revalidate / network-first。
- 静态壳资源强制更新:SW 更新时自动刷新页面。
下面给到可以直接改造的代码片段。
1. 保持当前的版本号失效机制(已实现)
你现在顶部这段已经是一个“强制失效”的手段:
const CACHE_VERSION = 'v0.0.5';
const CACHE_PREFIX = 'densenapp-static-';
const CACHE_NAME = `${CACHE_PREFIX}${CACHE_VERSION}`;
加上 activate 里按前缀清老版本:
self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
const keys = await caches.keys();
await Promise.all(
keys
.filter((key) => key.startsWith(CACHE_PREFIX) && key !== CACHE_NAME)
.map((key) => caches.delete(key))
);
const clientsList = await self.clients.matchAll({ type: 'window' });
for (const client of clientsList) {
client.postMessage({
type: 'SW_ACTIVATED',
version: CACHE_VERSION
});
}
await self.clients.claim();
})()
);
});
这保证了:
- 每次你修改前端、发版时只要改
CACHE_VERSION,旧缓存就会被干净删除; - 新版 SW 安装并激活后,走的都是新缓存。
这是第一道缓存失效线。
2. 避免长期使用“旧缓存”:改进 fetch 策略
现在你的静态资源用的是“纯 cache-first”:
if (request.destination === 'document'
|| request.destination === 'script'
|| request.destination === 'style'
|| request.destination === 'image'
|| request.destination === 'font') {
event.respondWith(cacheFirst(request));
return;
}
只靠版本号时,如果:
- 你忘记升级
CACHE_VERSION; - 或是某些资源 URL 没变;
用户会一直命中旧缓存。
可以对大部分静态资源采用更“新鲜”的策略,例如:
index.html用 network-first(总是尽量拿最新壳);- 其他 JS/CSS 用 stale-while-revalidate(先用缓存,后台刷新)。
2.1 示例:index.html 用 network-first
在 fetch 里单独拦 index.html:
self.addEventListener('fetch', (event) => {
const { request } = event;
if (request.method !== 'GET') {
return;
}
const url = new URL(request.url);
// 1. index.html:优先走网络,保证壳尽量最新
if (request.destination === 'document') {
event.respondWith(networkFirst(request, CACHE_NAME));
return;
}
// 2. 静态资源:stale-while-revalidate
if (request.destination === 'script'
|| request.destination === 'style'
|| request.destination === 'image'
|| request.destination === 'font') {
event.respondWith(staleWhileRevalidate(request, CACHE_NAME));
return;
}
// 3. 业务 API:network-first(你已经有一部分了),或者放行
if (url.pathname.startsWith('/api/getProduct')
|| url.pathname.startsWith('/api/help')
|| url.pathname.startsWith('/api/pro')
|| url.pathname.startsWith('/pro/customer')
|| url.pathname.startsWith('/files')
|| url.pathname.startsWith('/view')) {
event.respondWith(networkFirst(request, 'api-pos-cache'));
return;
}
if (url.pathname.startsWith('/api/')
|| url.pathname.startsWith('/pro/')
|| url.pathname.startsWith('/swagger')
|| url.pathname.includes('/signalr')) {
return;
}
});
对应实现两个策略函数:
function networkFirst(request, cacheName) {
return fetch(request)
.then((response) => {
if (response && response.status === 200) {
const clone = response.clone();
caches.open(cacheName).then((cache) => {
cache.put(request, clone);
});
}
return response;
})
.catch(() => {
return caches.match(request).then((cached) => {
if (cached) {
return cached;
}
// 对 HTML 做兜底
if (request.headers.get('accept')?.includes('text/html')) {
return caches.match('./index.html');
}
return new Response('网络不可用,且没有可用的缓存数据。', {
status: 503,
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
});
});
});
}
function staleWhileRevalidate(request, cacheName) {
return caches.match(request).then((cached) => {
const fetchPromise = fetch(request)
.then((response) => {
if (response && response.status === 200) {
const clone = response.clone();
caches.open(cacheName).then((cache) => {
cache.put(request, clone);
});
}
return response;
})
.catch(() => {
// 静态资源拉不下来就算了,主流程还有缓存支撑
return undefined;
});
// 有缓存:立即返回缓存,同时后台更新
if (cached) {
return cached;
}
// 没缓存:只能等网络
return fetchPromise.then((response) => {
if (response) {
return response;
}
// 最后兜底:如果是 HTML 再退回 index.html
if (request.headers.get('accept')?.includes('text/html')) {
return caches.match('./index.html');
}
return new Response('网络不可用,且本资源未被缓存。', {
status: 503,
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
});
});
});
}
这样:
index.html:每次优先网络 → 失败再用缓存 → 用户几乎总是拿最新壳;- JS/CSS 等:快速响应(缓存),同时后台拉新版本,下次访问就是新资源。
即便你忘记改 CACHE_VERSION,资源也会被后台刷新一轮。
3. 前端配合:SW 更新后自动刷新(确保真正用上最新资源)
你在 activate 里已经:
client.postMessage({
type: 'SW_ACTIVATED',
version: CACHE_VERSION
});
在前端(例如 js/index.js 或一个公共 JS)里加监听:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data?.type === 'SW_ACTIVATED') {
console.log('SW 已更新到版本:', event.data.version);
// 简单方案:立刻刷新,所有静态资源和 HTML 都切到最新
window.location.reload();
}
});
}
这样一来:
- 新版本部署 → 客户端加载新 SW → 下载新资源 → 清除旧缓存;
SW_ACTIVATED发出 → 页面刷新;- 保证“用户总是跑在最新前端代码 + 最新缓存资源”上。
4. 小结:对“缓存失效”的落地方案
在你当前 sw.js 上,为了确保尽量拿最新资源,可以总结为:
-
版本号控制 + 清缓存:
- 已有
CACHE_VERSION+activate清除旧版本缓存; - 发版记得改版本号。
- 已有
-
fetch 策略分级:
- 文档(HTML,尤其是
index.html):networkFirst,保证壳最新; - JS/CSS/图片:
staleWhileRevalidate,快速响应 + 后台更新; - 业务 API:按需用
networkFirst+ 单独 API 缓存名。
- 文档(HTML,尤其是
-
SW 更新后刷新页面:
- 利用
postMessage+ 前端window.location.reload(),保证真正启用新资源。
- 利用
如果你愿意,我可以按你最终想要的策略,直接给出一份完整替换版 sw.js,你只需要说明:
- 是否希望
index.html每次都尽量走网络; - 是否接受 SW 更新后“自动刷新一次页面”。
关联项目
FreeSql QQ群:4336577
BA & Blazor QQ群:795206915
Maui Blazor 中文社区 QQ群:645660665
知识共享许可协议
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名AlexChow(包含链接: https://github.com/densen2014 ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系 。
转载声明
本文来自博客园,作者:周创琳 AlexChow,转载请注明原文链接:https://chuna2.787528.xyz/densen2014/p/19623469
AlexChow
今日头条 | 博客园 | 知乎 | Gitee | GitHub


浙公网安备 33010602011771号