151 lines
4.2 KiB
JavaScript
Executable File
151 lines
4.2 KiB
JavaScript
Executable File
// Service Worker for InfoGenie App (PWA)
|
||
// 注意:PWA 必须在 HTTPS(或 localhost)下才能生效
|
||
|
||
const CACHE_VERSION = 'v2';
|
||
const PRECACHE_NAME = `infogenie-precache-${CACHE_VERSION}`;
|
||
const RUNTIME_NAME = `infogenie-runtime-${CACHE_VERSION}`;
|
||
|
||
const CORE_ASSETS = [
|
||
'/',
|
||
'/index.html',
|
||
'/manifest.json',
|
||
'/icons/icon-192.png',
|
||
'/icons/icon-512.png',
|
||
'/icons/icon-192-maskable.png',
|
||
'/icons/icon-512-maskable.png',
|
||
'/icons/apple-touch-icon.png',
|
||
'/icons/favicon-32.png',
|
||
'/icons/favicon-16.png'
|
||
];
|
||
|
||
async function safeAddAll(cache, urls) {
|
||
const uniqueUrls = Array.from(new Set(urls)).filter(Boolean);
|
||
const results = await Promise.allSettled(uniqueUrls.map(url => cache.add(url)));
|
||
const failures = results.filter(r => r.status === 'rejected');
|
||
if (failures.length > 0) {
|
||
console.warn('[SW] Some assets failed to cache:', failures.length);
|
||
}
|
||
}
|
||
|
||
async function precacheEntrypoints(cache) {
|
||
try {
|
||
const res = await fetch('/asset-manifest.json', { cache: 'no-store' });
|
||
if (!res.ok) return;
|
||
const manifest = await res.json();
|
||
|
||
const filesObj = manifest && typeof manifest === 'object' ? manifest.files : undefined;
|
||
const files = filesObj && typeof filesObj === 'object' ? Object.values(filesObj) : [];
|
||
const entrypoints = Array.isArray(manifest.entrypoints) ? manifest.entrypoints : [];
|
||
|
||
const urls = [...files, ...entrypoints]
|
||
.filter(p => typeof p === 'string')
|
||
.map(p => (p.startsWith('/') ? p : `/${p}`))
|
||
.filter(p => !p.endsWith('.map'));
|
||
|
||
await safeAddAll(cache, urls);
|
||
} catch (err) {
|
||
console.warn('[SW] Failed to precache entrypoints:', err);
|
||
}
|
||
}
|
||
|
||
self.addEventListener('install', event => {
|
||
self.skipWaiting();
|
||
event.waitUntil(
|
||
(async () => {
|
||
const cache = await caches.open(PRECACHE_NAME);
|
||
await safeAddAll(cache, CORE_ASSETS);
|
||
await precacheEntrypoints(cache);
|
||
})()
|
||
);
|
||
});
|
||
|
||
self.addEventListener('activate', event => {
|
||
event.waitUntil(
|
||
(async () => {
|
||
const cacheNames = await caches.keys();
|
||
await Promise.all(
|
||
cacheNames.map(name => {
|
||
if (name !== PRECACHE_NAME && name !== RUNTIME_NAME) {
|
||
return caches.delete(name);
|
||
}
|
||
return undefined;
|
||
})
|
||
);
|
||
|
||
await self.clients.claim();
|
||
})()
|
||
);
|
||
});
|
||
|
||
function isNavigationRequest(request) {
|
||
return request.mode === 'navigate' || request.destination === 'document';
|
||
}
|
||
|
||
function shouldHandleRequest(url, request) {
|
||
if (request.method !== 'GET') return false;
|
||
if (url.origin !== self.location.origin) return false;
|
||
return true;
|
||
}
|
||
|
||
self.addEventListener('fetch', event => {
|
||
const url = new URL(event.request.url);
|
||
if (!shouldHandleRequest(url, event.request)) return;
|
||
|
||
// 不缓存后端 API(如需缓存请在这里加规则)
|
||
if (url.pathname.startsWith('/api')) return;
|
||
|
||
// 页面请求:优先网络,离线回退到缓存
|
||
if (isNavigationRequest(event.request)) {
|
||
event.respondWith(
|
||
(async () => {
|
||
try {
|
||
const networkResponse = await fetch(event.request);
|
||
const cache = await caches.open(RUNTIME_NAME);
|
||
cache.put(event.request, networkResponse.clone());
|
||
return networkResponse;
|
||
} catch (err) {
|
||
const cached = await caches.match(event.request);
|
||
return cached || caches.match('/index.html');
|
||
}
|
||
})()
|
||
);
|
||
return;
|
||
}
|
||
|
||
// 静态资源:stale-while-revalidate
|
||
event.respondWith(
|
||
(async () => {
|
||
const cached = await caches.match(event.request);
|
||
const cache = await caches.open(RUNTIME_NAME);
|
||
|
||
const networkFetch = (async () => {
|
||
try {
|
||
const response = await fetch(event.request);
|
||
if (response && response.ok) {
|
||
cache.put(event.request, response.clone());
|
||
}
|
||
return response;
|
||
} catch (err) {
|
||
return undefined;
|
||
}
|
||
})();
|
||
|
||
if (cached) {
|
||
event.waitUntil(networkFetch);
|
||
return cached;
|
||
}
|
||
|
||
const response = await networkFetch;
|
||
if (response) return response;
|
||
|
||
return new Response('', { status: 504, statusText: 'Offline' });
|
||
})()
|
||
);
|
||
});
|
||
|
||
self.addEventListener('message', event => {
|
||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||
self.skipWaiting();
|
||
}
|
||
});
|