企业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 里,核心就是“不同类型请求用不同策略”:
-
静态资源(壳):
- 通常用 precache + cache-first(你现在就是这样),保证离线能启动应用。
-
经常变更的数据 API:
- 通常用 network-first:
- 在线:优先走网络,失败再回退缓存;
- 离线:用上一次的缓存数据兜底(如果有)。
- 通常用 network-first:
-
不关键、可以过期的资源(图片、列表页 HTML 等):
- 可以用 stale-while-revalidate:
- 先立即返回缓存(即使旧),同时后台拉最新、更新缓存;
- 下次访问再用新的。
- 可以用 stale-while-revalidate:
-
只读、几乎不变的资源(字体、库文件等):
- 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 / 店内设备,网络容易不稳定;
推荐的简化策略:
- 静态壳:维持你现在的
precache + cache-first + HTML 兜底(已经 OK)。 - API:先不缓存,只要是
/api/直接 passthrough(你现在就是),保证不会因为 SW 搞坏接口行为。 - 等你想要“离线可用数据”时,再针对某个小范围 API 单独加
network-first缓存(例如只给查询接口缓存,避免订单类操作被离线缓存污染)。
5. 小结
在 Service Worker 里实现离线缓存策略,核心就是:
- install 阶段预缓存“壳”(你已实现);
- fetch 阶段按类型选择策略:
- 静态资源:cache-first,离线可用;
- 非关键 API:直接放行;
- 某些业务 API(需要离线数据):network-first + 缓存兜底。
你当前的 sw.js 已经是一个合理的“静态壳离线缓存策略”。如果你告诉我 API 的实际路径(例如 /api/pos/、/api/pda/),我可以直接把一段 networkFirst 策略完整插进你现在的 sw.js,保证和现有逻辑风格一致、可直接复制替换。
关联项目
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/19623468
AlexChow
今日头条 | 博客园 | 知乎 | Gitee | GitHub


浙公网安备 33010602011771号