// 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(); } });