Files
mengya-nav/frontend/public/sw.js
2026-06-24 22:10:28 +08:00

182 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* PWA 缓存策略(与 React/Vite 产物配合shell 仅预缓存关键静态,其余运行时 stale-while-revalidate
*/
const CACHE_VERSION = 'v5';
const STATIC_CACHE = `mengya-static-${CACHE_VERSION}`;
const RUNTIME_CACHE = `mengya-runtime-${CACHE_VERSION}`;
const API_CACHE = `mengya-api-${CACHE_VERSION}`;
const OFFLINE_FALLBACK = '/offline.html';
/** Vite 构建后入口由服务器返回 index.html此处预缓存根路径与离线 fallback */
const APP_SHELL = ['/', '/offline.html', '/manifest.webmanifest', '/logo.png', '/favicon.ico'];
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(STATIC_CACHE).then((cache) => cache.addAll(APP_SHELL)));
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames
.filter((name) => ![STATIC_CACHE, RUNTIME_CACHE, API_CACHE].includes(name))
.map((name) => caches.delete(name))
);
if ('navigationPreload' in self.registration) {
await self.registration.navigationPreload.enable();
}
await self.clients.claim();
})()
);
});
self.addEventListener('message', (event) => {
if (event.data === 'SKIP_WAITING') {
self.skipWaiting();
}
});
self.addEventListener('fetch', (event) => {
const { request } = event;
if (request.method !== 'GET') {
return;
}
const url = new URL(request.url);
if (request.mode === 'navigate') {
event.respondWith(handleNavigationRequest(event));
return;
}
if (url.origin !== self.location.origin) {
return;
}
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request, API_CACHE));
return;
}
if (isStaticAssetRequest(request, url)) {
event.respondWith(staleWhileRevalidate(request, RUNTIME_CACHE));
return;
}
event.respondWith(cacheFirst(request, RUNTIME_CACHE));
});
async function handleNavigationRequest(event) {
try {
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
return preloadResponse;
}
const networkResponse = await fetch(event.request);
if (networkResponse && networkResponse.ok) {
const cache = await caches.open(RUNTIME_CACHE);
cache.put(event.request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
const cachedPage = await caches.match(event.request);
if (cachedPage) {
return cachedPage;
}
const offlineResponse = await caches.match(OFFLINE_FALLBACK);
return offlineResponse || new Response('Offline', { status: 503 });
}
}
async function networkFirst(request, cacheName) {
const cache = await caches.open(cacheName);
try {
const response = await fetch(request);
if (shouldCacheResponse(response)) {
cache.put(request, response.clone());
}
return response;
} catch (error) {
const cached = await cache.match(request);
if (cached) {
return cached;
}
if (request.mode === 'navigate') {
const offlineResponse = await caches.match(OFFLINE_FALLBACK);
if (offlineResponse) {
return offlineResponse;
}
}
return new Response(JSON.stringify({ message: 'Network unavailable' }), {
status: 503,
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
});
}
}
async function staleWhileRevalidate(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
const networkPromise = fetch(request)
.then((response) => {
if (shouldCacheResponse(response)) {
cache.put(request, response.clone());
}
return response;
})
.catch(() => undefined);
return cached || networkPromise || new Response('Not found', { status: 404 });
}
async function cacheFirst(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
if (cached) {
return cached;
}
const response = await fetch(request);
if (shouldCacheResponse(response)) {
cache.put(request, response.clone());
}
return response;
}
function isStaticAssetRequest(request, url) {
if (
request.destination === 'script' ||
request.destination === 'style' ||
request.destination === 'image' ||
request.destination === 'font'
) {
return true;
}
return (
url.pathname.endsWith('.css') ||
url.pathname.endsWith('.js') ||
url.pathname.endsWith('.png') ||
url.pathname.endsWith('.jpg') ||
url.pathname.endsWith('.jpeg') ||
url.pathname.endsWith('.svg') ||
url.pathname.endsWith('.ico') ||
url.pathname.endsWith('.webmanifest') ||
url.pathname.endsWith('.woff2')
);
}
function shouldCacheResponse(response) {
return Boolean(response && (response.status === 200 || response.type === 'opaque'));
}