企业H5站点升级PWA (七)
如何在 Service Worker 中实现缓存的版本控制和更新策略,以确保用户始终获取到最新的资源?
下面直接结合你当前的 sw.js,给一套可落地的“版本控制 + 更新策略”,目标是:
- 有明确的缓存版本号,发版时可控地失效旧缓存;
index.html等壳尽量拿最新;- JS/CSS 等静态资源不会长期卡在老版本;
- SW 更新后,用户可以自动/半自动切到新版本。
1. 版本号控制:用 CACHE_VERSION 驱动缓存失效
你现在已经有:
const CACHE_VERSION = 'v0.0.5';
const CACHE_PREFIX = 'densenapp-static-';
const CACHE_NAME = `${CACHE_PREFIX}${CACHE_VERSION}`;
这是第一层“版本控制”。建议约定:
- 每次前端发版,手动提升
CACHE_VERSION(如v0.0.6); - 让
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))
);
// 通知所有客户端:SW 已激活新版本
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();
})()
);
});
这样只要版本号变了:
- 安装新 SW 时会建新缓存
densenapp-static-v0.0.6; - 激活时会删掉所有旧的
densenapp-static-*。
2. 更新策略:不同资源用不同缓存策略
2.1 建议策略组合
-
HTML(尤其是
index.html)
使用 network-first:优先从网络拉,失败再用缓存。这样壳页面几乎总是最新。 -
JS/CSS/图片等静态资源
使用 stale-while-revalidate:- 有缓存时立即返回缓存(响应快);
- 同时后台去拉网络,如果成功则更新缓存;
- 下次访问时就是新资源。
即便你忘了改CACHE_VERSION,它也会逐步更新。
-
API
- 你已经对部分 API 用了
networkFirst(request, 'api-pos-cache'); - 其它
/api/直接放行,不缓存(避免数据过期问题)。
- 你已经对部分 API 用了
2.2 在现有 fetch 上的改造示例
可以把当前 fetch 改成类似这样(保持你现有 API 处理逻辑):
self.addEventListener('fetch', (event) => {
const { request } = event;
if (request.method !== 'GET') {
return;
}
const url = new URL(request.url);
// 1. HTML 文档:network-first,保证壳尽量最新
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;
}
// 4. 其它 API 直接放行,避免被 SW 干扰
if (url.pathname.startsWith('/api/')
|| url.pathname.startsWith('/pro/')
|| url.pathname.startsWith('/swagger')
|| url.pathname.includes('/signalr')) {
return;
}
});
然后在底部增加一个 staleWhileRevalidate,并保留你原来的 networkFirst(可以复用):
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' }
});
});
});
}
这样配合上面的版本号策略,即使你偶尔忘记改
CACHE_VERSION,静态资源也会被后台拉新。
3. 前端配合:SW 更新后自动或提示刷新
你的 activate 中已经通过 postMessage 通知页面:
client.postMessage({
type: 'SW_ACTIVATED',
version: CACHE_VERSION
});
在前端公共 JS(例如 js/index.js 或某个引导脚本)里加一个监听,可以选择:
- 简单粗暴:直接自动刷新;
- 或者弹个“发现新版本,点击刷新”的提示。
一个自动刷新的最简版:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data?.type === 'SW_ACTIVATED') {
console.log('Service Worker 已更新到版本:', event.data.version);
// 直接刷新页面,让用户切到新资源
window.location.reload();
}
});
}
这样就形成了完整链路:
- 你改了前端代码 → 提升
CACHE_VERSION→ 部署; - 客户端加载新 SW:
- 安装新缓存(
PRECACHE_URLS); - 激活时清旧缓存,
postMessage给页面;
- 安装新缓存(
- 页面收到
SW_ACTIVATED,reload一次; - 用户立刻跑在最新资源 + 最新缓存上。
4. 实际发版时你需要做的事
-
修改前端代码后:
- 更新
CACHE_VERSION(如从v0.0.5改到v0.0.6); - 如果你对静态文件做版本号(
index.js?v=0.0.6)或 hash,也一并更新。
- 更新
-
发布(Kestrel / WinForms 宿主的
wwwroot同步上去)。 -
客户端行为:
- 下次打开时:
- 新 SW 安装并预缓存;
- 删除旧缓存;
- 通知页面(
SW_ACTIVATED); - 页面刷新,所有资源切新。
- 下次打开时:
如果你希望,我可以按你刚才这份 sw.js 直接给一个“已经嵌好 networkFirst + staleWhileRevalidate”的完整版本,你只要整体替换就可以。
关联项目
FreeSql QQ群:4336577
BA & Blazor QQ群:795206915
Maui Blazor 中文社区 QQ群:645660665
知识共享许可协议
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名AlexChow(包含链接: https://github.com/densen2014 ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系 。
转载声明
本文来自博客园,作者:周创琳 AlexChow,转载请注明原文链接:https://www.cnblogs.com/densen2014/p/19623471
AlexChow
今日头条 | 博客园 | 知乎 | Gitee | GitHub


浙公网安备 33010602011771号