- manifest.webmanifest: standalone, start_url /?source=pwa, theme #312E81, 4 icons - sw.js v1: 预缓存骨架(11项), 导航+静态 network-first, /api/* 与 /mcp/* 不缓存 - icons: 192/512 any + maskable + apple-touch-icon - 实测: SW activated+controlling, installabilityErrors=[], 离线重载骨架正常渲染
93 lines
3.4 KiB
JavaScript
93 lines
3.4 KiB
JavaScript
/**
|
||
* 工时薪酬助手 — Service Worker
|
||
*
|
||
* 职责(刻意收得很窄):
|
||
* 1. 预缓存页面骨架(index.html / sync.js / payroll.js / vendor 两件 / manifest / 图标),
|
||
* 保证离线或服务器彻底挂掉时页面能完整启动。
|
||
* 2. 导航请求走 network-first + 离线兜底 index.html:在线时永远拿最新版,
|
||
* 离线时用预缓存的骨架(本地记账照常,数据在 localStorage,与 SW 无关)。
|
||
* 3. 静态资源 network-first:在线优先最新(no-cache 本来就要求「用前先问」),
|
||
* 网络失败才回缓存 —— SW 不改变现有缓存策略,只补「彻底断网」这一层。
|
||
* 4. /api/* 与 /mcp/sse 一律不碰:同步引擎自己有超时与 offline 处理,
|
||
* SW 绝不缓存 API 响应,避免「读到旧数据以为同步成功」的假象。
|
||
*
|
||
* 版本升级:静态资源 no-cache+ETag,导航每次都回源,新 index.html 引用的
|
||
* vendor ?v= 指纹变化会触发重下,PRECACHE 键值同步更新即可。旧缓存条目
|
||
* activate 时统一清理,不做遗留兜底。
|
||
*/
|
||
const VERSION = 'v1';
|
||
const PRECACHE = `wht-precache-${VERSION}`;
|
||
const RUNTIME = `wht-runtime-${VERSION}`;
|
||
|
||
// 页面骨架:缺一个离线就是白屏,全部预缓存。
|
||
const PRECACHE_URLS = [
|
||
'/',
|
||
'/index.html',
|
||
'/sync.js',
|
||
'/shared/payroll.js',
|
||
'/vendor/vue.global.prod.js',
|
||
'/vendor/tailwind.css',
|
||
'/manifest.webmanifest',
|
||
'/icons/icon-192.png',
|
||
'/icons/icon-512.png',
|
||
'/icons/icon-maskable-192.png',
|
||
'/icons/icon-maskable-512.png'
|
||
];
|
||
|
||
self.addEventListener('install', (event) => {
|
||
event.waitUntil(
|
||
caches.open(PRECACHE)
|
||
.then((cache) => cache.addAll(PRECACHE_URLS))
|
||
// 骨架里任何一个取不到就放弃安装:宁可没有 SW 也不能装出半个能用的
|
||
.then(() => self.skipWaiting())
|
||
);
|
||
});
|
||
|
||
self.addEventListener('activate', (event) => {
|
||
event.waitUntil(
|
||
caches.keys()
|
||
.then((keys) => Promise.all(
|
||
keys.filter((k) => k !== PRECACHE && k !== RUNTIME)
|
||
.map((k) => caches.delete(k))
|
||
))
|
||
.then(() => self.clients.claim())
|
||
);
|
||
});
|
||
|
||
self.addEventListener('message', (event) => {
|
||
if (event.data === 'SKIP_WAITING') self.skipWaiting();
|
||
});
|
||
|
||
/** network-first:先在线取,失败回运行时缓存,再失败回预缓存。 */
|
||
async function networkFirst(request, cacheName) {
|
||
const cache = await caches.open(cacheName);
|
||
try {
|
||
const res = await fetch(request);
|
||
if (res && res.ok) cache.put(request, res.clone());
|
||
return res;
|
||
} catch (e) {
|
||
const cached = await cache.match(request, { ignoreSearch: request.mode === 'navigate' });
|
||
if (cached) return cached;
|
||
// 导航请求最终兜底:预缓存的 index.html(ignoreSearch 吞掉 ?source=pwa 等查询)
|
||
const pre = await caches.open(PRECACHE);
|
||
const fallback = await pre.match('/index.html', { ignoreSearch: true });
|
||
if (fallback) return fallback;
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
self.addEventListener('fetch', (event) => {
|
||
const url = new URL(event.request.url);
|
||
|
||
// 只管同源 GET;跨域与 /api/*、/mcp/* 直接放行给网络
|
||
if (url.origin !== self.location.origin) return;
|
||
if (event.request.method !== 'GET') return;
|
||
if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/mcp/')) return;
|
||
|
||
if (event.request.mode === 'navigate') {
|
||
event.respondWith(networkFirst(event.request, RUNTIME));
|
||
return;
|
||
}
|
||
|
||
event.respondWith(networkFirst(event.request, RUNTIME));
|
||
}); |