Maui Blazor 中文社区 QQ群:645660665

企业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 建议策略组合

  1. HTML(尤其是 index.html
    使用 network-first:优先从网络拉,失败再用缓存。这样壳页面几乎总是最新。

  2. JS/CSS/图片等静态资源
    使用 stale-while-revalidate

    • 有缓存时立即返回缓存(响应快);
    • 同时后台去拉网络,如果成功则更新缓存;
    • 下次访问时就是新资源。
      即便你忘了改 CACHE_VERSION,它也会逐步更新。
  3. API

    • 你已经对部分 API 用了 networkFirst(request, 'api-pos-cache')
    • 其它 /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();
        }
    });
}

这样就形成了完整链路:

  1. 你改了前端代码 → 提升 CACHE_VERSION → 部署;
  2. 客户端加载新 SW:
    • 安装新缓存(PRECACHE_URLS);
    • 激活时清旧缓存,postMessage 给页面;
  3. 页面收到 SW_ACTIVATEDreload 一次;
  4. 用户立刻跑在最新资源 + 最新缓存上。

4. 实际发版时你需要做的事

  1. 修改前端代码后

    • 更新 CACHE_VERSION(如从 v0.0.5 改到 v0.0.6);
    • 如果你对静态文件做版本号(index.js?v=0.0.6)或 hash,也一并更新。
  2. 发布(Kestrel / WinForms 宿主的 wwwroot 同步上去)。

  3. 客户端行为

    • 下次打开时:
      • 新 SW 安装并预缓存;
      • 删除旧缓存;
      • 通知页面(SW_ACTIVATED);
      • 页面刷新,所有资源切新。

如果你希望,我可以按你刚才这份 sw.js 直接给一个“已经嵌好 networkFirst + staleWhileRevalidate”的完整版本,你只要整体替换就可以。

posted @ 2026-02-18 20:06  AlexChow  阅读(139)  评论(0)    收藏  举报