Maui Blazor 中文社区 QQ群:645660665

企业H5站点升级PWA (五)

如何在 Service Worker 中实现离线缓存的策略?

下面直接结合你当前的 sw.js,用几种常用策略说明“离线缓存”应该怎么设计,以及在你这个项目里怎么写。

你现在实现的是典型的 cache-first 静态资源缓存 + HTML 兜底,已经是一个可用的离线策略了。可以在这个基础上按需要细化。


1. 你当前已经实现的离线策略

PRECACHE_URLS 里的资源在 install 阶段就被缓存:

const CACHE_VERSION = 'v0.0.2';
const CACHE_PREFIX = 'densenapp-static-';
const CACHE_NAME = `${CACHE_PREFIX}${CACHE_VERSION}`;

const PRECACHE_URLS = [
    './',
    './index.html',
    './index.css',
    './index.js',
    './homeConfig.js',
    './pda.js',
    './auth.js',
    './bootstrap.bundle.min.js',
    './densen26.jpg',
    './manifest.json'
];

self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(CACHE_NAME).then((cache) => {
            return cache.addAll(PRECACHE_URLS);
        })
    );
    self.skipWaiting();
});

fetch 里使用 cache-first 策略,并在离线时对 HTML 兜底到首页:

self.addEventListener('fetch', (event) => {
    const { request } = event;

    if (request.method !== 'GET') {
        return;
    }

    const url = new URL(request.url);

    if (url.pathname.startsWith('/api/')
        || url.pathname.startsWith('/swagger')
        || url.pathname.includes('/signalr')) {
        return;
    }

    event.respondWith(
        caches.match(request).then((cached) => {
            if (cached) {
                return cached; // 离线时优先命中这里
            }

            return fetch(request)
                .then((response) => {
                    if (!response || response.status !== 200 || response.type === 'opaque') {
                        return response;
                    }

                    const clone = response.clone();
                    caches.open(CACHE_NAME).then((cache) => {
                        cache.put(request, clone);
                    });

                    return response;
                })
                .catch(() => {
                    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 + index.js + css + 主要 js)离线可用;
  • 后续访问的静态子页面(如 pda.html,如果被访问过)也会被缓存,离线时还能打开;
  • API 请求(/api/...)不被拦截,离线会直接失败——业务数据目前不做离线缓存。

2. 常见离线缓存策略有哪些?

在 Service Worker 里,核心就是“不同类型请求用不同策略”:

  1. 静态资源(壳)

    • 通常用 precache + cache-first(你现在就是这样),保证离线能启动应用。
  2. 经常变更的数据 API

    • 通常用 network-first
      • 在线:优先走网络,失败再回退缓存;
      • 离线:用上一次的缓存数据兜底(如果有)。
  3. 不关键、可以过期的资源(图片、列表页 HTML 等):

    • 可以用 stale-while-revalidate
      • 先立即返回缓存(即使旧),同时后台拉最新、更新缓存;
      • 下次访问再用新的。
  4. 只读、几乎不变的资源(字体、库文件等):

    • cache-only 或长期 cache-first 即可。

3. 在你当前 sw.js 上细化离线策略示例

3.1 静态资源继续用 cache-first(维持现在的逻辑)

这部分基本不用改,你现有的处理已经是合理的“离线静态壳策略”。

如果想更明确地区分,可以按类型拆一点逻辑,比如:

self.addEventListener('fetch', (event) => {
    const { request } = event;

    if (request.method !== 'GET') {
        return;
    }

    const url = new URL(request.url);

    // 放过 API
    if (url.pathname.startsWith('/api/')
        || url.pathname.startsWith('/swagger')
        || url.pathname.includes('/signalr')) {
        return;
    }

    // 对 HTML/CSS/JS/图片等使用统一 cache-first
    if (request.destination === 'document'
        || request.destination === 'script'
        || request.destination === 'style'
        || request.destination === 'image'
        || request.destination === 'font') {

        event.respondWith(cacheFirst(request));
        return;
    }

    // 其他类型按需处理,或者直接 return 让浏览器自己走
});

function cacheFirst(request) {
    return caches.match(request).then((cached) => {
        if (cached) {
            return cached;
        }

        return fetch(request)
            .then((response) => {
                if (!response || response.status !== 200 || response.type === 'opaque') {
                    return response;
                }

                const clone = response.clone();
                caches.open(CACHE_NAME).then((cache) => {
                    cache.put(request, clone);
                });

                return response;
            })
            .catch(() => {
                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' }
                });
            });
    });
}

注意:多出来的 cacheFirst 函数纯粹是抽取逻辑,方便你后面再加 networkFirst 等。

3.2 如果要给某些 API 做“离线数据缓存”(可选增强)

假设你的业务 API 在 /api/pos/ 下,你希望:

  • 在线时:优先拉网络,顺便把结果缓存下来;
  • 离线时:如果有历史缓存,就用最近一次数据兜底。

可以在 fetch 中专门处理这类请求:

self.addEventListener('fetch', (event) => {
    const { request } = event;
    if (request.method !== 'GET') {
        return;
    }

    const url = new URL(request.url);

    // 针对某些 API 使用 network-first
    if (url.pathname.startsWith('/api/pos/')) {
        event.respondWith(networkFirst(request, 'api-pos-cache'));
        return;
    }

    // 其他逻辑...(静态资源 cache-first 等)
});

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;
                }
                return new Response('网络不可用,且没有可用的缓存数据。', {
                    status: 503,
                    headers: { 'Content-Type': 'text/plain; charset=utf-8' }
                });
            });
        });
}

这样,和当前前端逻辑 (index.js + pda.js 等) 完全解耦,前端依然 fetch('/api/pos/...'),只是在离线时,Service Worker 能试图给出一个“上次的结果”。


4. 实战建议:你的项目里离线策略可以怎么定

结合你现有结构:

  • WinForms 宿主 + wwwroot/index.html
  • 前端通过 API_BASE 调用后端;
  • 主要运行环境可能是 PDA / 店内设备,网络容易不稳定;

推荐的简化策略:

  1. 静态壳:维持你现在的 precache + cache-first + HTML 兜底(已经 OK)。
  2. API:先不缓存,只要是 /api/ 直接 passthrough(你现在就是),保证不会因为 SW 搞坏接口行为。
  3. 等你想要“离线可用数据”时,再针对某个小范围 API 单独加 network-first 缓存(例如只给查询接口缓存,避免订单类操作被离线缓存污染)。

5. 小结

在 Service Worker 里实现离线缓存策略,核心就是:

  • install 阶段预缓存“壳”(你已实现);
  • fetch 阶段按类型选择策略
    • 静态资源:cache-first,离线可用;
    • 非关键 API:直接放行;
    • 某些业务 API(需要离线数据):network-first + 缓存兜底。

你当前的 sw.js 已经是一个合理的“静态壳离线缓存策略”。如果你告诉我 API 的实际路径(例如 /api/pos//api/pda/),我可以直接把一段 networkFirst 策略完整插进你现在的 sw.js,保证和现有逻辑风格一致、可直接复制替换。

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