diff --git a/public/sw.js b/public/sw.js index 781855a..f531a46 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,300 +1,191 @@ -// Service Worker - PWA 支持(增强缓存版) -// 尽可能多地缓存文件以提升加载速度 -const SW_VERSION = self.__SW_VERSION__ || Date.now().toString(36); -const CACHE_NAME = `searchgal-cache-${SW_VERSION}`; +// Service Worker - PWA 支持 +// 版本由构建工具注入,开发时使用时间戳 +const SW_VERSION = self.__SW_VERSION__ || Date.now().toString(36) +const CACHE_NAME = `searchgal-${SW_VERSION}` -// 缓存静态资源(尽可能多) -const CACHEABLE_PATTERNS = [ - /\/assets\/.*\.(js|css|mjs)(\?.*)?$/i, // Vite 构建的静态资源 - /\.(woff2?|ttf|otf|eot)(\?.*)?$/i, // 字体 - /\.(png|jpg|jpeg|gif|webp|svg|ico|avif)(\?.*)?$/i, // 图片 - /\.(mp3|wav|ogg|m4a|aac|flac)(\?.*)?$/i, // 音频(snd-lib 音效) - /\.(mp4|webm|ogv)(\?.*)?$/i, // 视频 - /\.(json|xml|txt)(\?.*)?$/i, // 数据文件 - /\.(wasm)(\?.*)?$/i, // WebAssembly - /\/manifest\.json$/i, // PWA manifest - /\/favicon/i, // Favicon 相关 - /\/apple-touch-icon/i, // iOS 图标 - /\/android-chrome/i, // Android 图标 -]; - -// 预缓存的核心资源(安装时缓存) -const PRECACHE_URLS = [ - '/', - '/index.html', - '/manifest.json', -]; +// 可缓存的静态资源模式 +const CACHEABLE_EXTS = /\.(js|css|mjs|woff2?|ttf|otf|eot|png|jpg|jpeg|gif|webp|svg|ico|avif|mp3|wav|ogg|m4a|aac|flac|wasm)(\?.*)?$/i +const CACHEABLE_PATHS = /\/(manifest\.json|favicon|apple-touch-icon|android-chrome)/i // 永不缓存 -const NO_CACHE_PATTERNS = [ - /\/api\//, - /\/sw\.js$/, - /sockjs-node/, - /__vite/, - /hot-update/, - /\.map$/, // Source maps -]; +const NO_CACHE = /\/(api|sw\.js|sockjs-node|__vite|hot-update)|\.map$/ + +// 预缓存资源 +const PRECACHE = ['/', '/index.html', '/manifest.json'] // ============================================ -// 安装事件 - 预缓存核心资源 +// 生命周期事件 // ============================================ -self.addEventListener('install', (event) => { - console.log(`[SW] Installing version ${SW_VERSION}`); - - // 预缓存失败不应阻止 service worker 安装 - const precachePromise = caches.open(CACHE_NAME) - .then((cache) => { - console.log('[SW] Precaching core resources'); - return cache.addAll(PRECACHE_URLS); - }) - .catch((err) => { - console.warn('[SW] Precache failed:', err); - }); - - event.waitUntil( - precachePromise.then(() => self.skipWaiting()) - ); -}); -// ============================================ -// 激活事件 - 清理旧缓存 -// ============================================ -self.addEventListener('activate', (event) => { - console.log(`[SW] Activating version ${SW_VERSION}`); - - event.waitUntil( +self.addEventListener('install', (e) => { + console.log(`[SW] Installing v${SW_VERSION}`) + e.waitUntil( + caches.open(CACHE_NAME) + .then((c) => c.addAll(PRECACHE).catch(() => {})) + .then(() => self.skipWaiting()) + ) +}) + +self.addEventListener('activate', (e) => { + console.log(`[SW] Activating v${SW_VERSION}`) + e.waitUntil( caches.keys() - .then((names) => Promise.all( - names - .filter((name) => name.startsWith('searchgal-cache-') && name !== CACHE_NAME) - .map((name) => caches.delete(name)) + .then((keys) => Promise.all( + keys.filter((k) => k.startsWith('searchgal-') && k !== CACHE_NAME).map((k) => caches.delete(k)) )) .then(() => self.clients.claim()) - ); -}); + ) +}) // ============================================ // 消息处理 // ============================================ -self.addEventListener('message', (event) => { - const { type } = event.data || {}; - + +self.addEventListener('message', (e) => { + const { type } = e.data || {} if (type === 'GET_VERSION') { - event.ports[0]?.postMessage({ version: SW_VERSION }); + e.ports[0]?.postMessage({ version: SW_VERSION }) } else if (type === 'SKIP_WAITING') { - self.skipWaiting(); + self.skipWaiting() } else if (type === 'CLEAR_CACHE') { - event.waitUntil( + e.waitUntil( caches.keys() - .then((names) => Promise.all(names.map((name) => caches.delete(name)))) - .then(() => event.ports[0]?.postMessage({ success: true })) - ); + .then((keys) => Promise.all(keys.map((k) => caches.delete(k)))) + .then(() => e.ports[0]?.postMessage({ success: true })) + ) } -}); +}) // ============================================ // 请求拦截 // ============================================ -self.addEventListener('fetch', (event) => { - const { request } = event; - const url = new URL(request.url); - // 跳过非 GET 请求 - if (request.method !== 'GET') return; - - // 跳过非 HTTP(S) 请求 - if (!url.protocol.startsWith('http')) return; - - // 跳过永不缓存的模式 - if (NO_CACHE_PATTERNS.some((p) => p.test(url.href))) return; +self.addEventListener('fetch', (e) => { + const { request } = e + const url = new URL(request.url) - // 同源请求 - if (url.origin === location.origin) { - // HTML 页面请求:网络优先,离线时显示提示 - if (request.destination === 'document' || url.pathname === '/' || url.pathname.endsWith('.html')) { - event.respondWith(handlePageRequest(request)); - return; + // 跳过:非 GET、非 HTTP、永不缓存、跨域图片 + if ( + request.method !== 'GET' || + !url.protocol.startsWith('http') || + NO_CACHE.test(url.href) || + (url.origin !== location.origin && request.destination === 'image') + ) return + + const isSameOrigin = url.origin === location.origin + const isDocument = request.destination === 'document' || url.pathname === '/' || url.pathname.endsWith('.html') + const isCacheable = CACHEABLE_EXTS.test(url.pathname) || CACHEABLE_PATHS.test(url.pathname) + + if (isSameOrigin) { + if (isDocument) { + // HTML:网络优先,离线返回提示页 + e.respondWith(networkFirstWithOffline(request)) + } else if (isCacheable) { + // 静态资源:缓存优先 + e.respondWith(cacheFirst(request)) + } else { + // 其他:网络优先 + e.respondWith(networkFirst(request)) } - - // 可缓存的静态资源:缓存优先(加速) - if (CACHEABLE_PATTERNS.some((p) => p.test(url.pathname) || p.test(url.href))) { - event.respondWith(cacheFirst(request)); - return; - } - - // 同源其他资源:网络优先,但也缓存 - event.respondWith(networkFirst(request)); - return; + } else if (isCacheable) { + // 跨域可缓存资源 + e.respondWith(cacheFirstCrossOrigin(request)) } +}) - // 跨域资源:仅缓存可缓存的静态资源(如 CDN 资源) - if (CACHEABLE_PATTERNS.some((p) => p.test(url.pathname) || p.test(url.href))) { - event.respondWith(cacheFirstCrossOrigin(request)); - return; - } -}); +// ============================================ +// 缓存策略 +// ============================================ -/** - * 处理页面请求 - 离线时显示联网提示 - */ -async function handlePageRequest(request) { +async function cacheFirst(req) { + const cached = await caches.match(req) + if (cached) return cached try { - const response = await fetch(request); - return response; + const res = await fetch(req) + if (res.ok) { + const cache = await caches.open(CACHE_NAME) + cache.put(req, res.clone()) + } + return res } catch { - // 离线 - 返回联网提示页面 - return createOfflinePage(); + return new Response('', { status: 503 }) } } -/** - * 缓存优先(用于静态资源加速) - */ -async function cacheFirst(request) { - const cached = await caches.match(request); - if (cached) return cached; - +async function networkFirst(req) { try { - const response = await fetch(request); - if (response.ok) { - const cache = await caches.open(CACHE_NAME); - cache.put(request, response.clone()); + const res = await fetch(req) + if (res.ok) { + const cache = await caches.open(CACHE_NAME) + cache.put(req, res.clone()) } - return response; + return res } catch { - return new Response('', { status: 503 }); + return (await caches.match(req)) || new Response('', { status: 503 }) } } -/** - * 网络优先 - */ -async function networkFirst(request) { +async function networkFirstWithOffline(req) { try { - const response = await fetch(request); - if (response.ok) { - const cache = await caches.open(CACHE_NAME); - cache.put(request, response.clone()); - } - return response; + return await fetch(req) } catch { - const cached = await caches.match(request); - return cached || new Response('', { status: 503 }); + return offlinePage() } } -/** - * 缓存优先(跨域资源) - */ -async function cacheFirstCrossOrigin(request) { - const cached = await caches.match(request); - if (cached) return cached; - +async function cacheFirstCrossOrigin(req) { + const cached = await caches.match(req) + if (cached) return cached try { - // 跨域请求需要设置 mode - const response = await fetch(request, { mode: 'cors', credentials: 'omit' }); - if (response.ok && response.type !== 'opaque') { - const cache = await caches.open(CACHE_NAME); - cache.put(request, response.clone()); + const res = await fetch(req, { mode: 'cors', credentials: 'omit' }) + if (res.ok && res.type !== 'opaque') { + const cache = await caches.open(CACHE_NAME) + cache.put(req, res.clone()) } - return response; + return res } catch { - // 跨域失败时返回空响应 - return new Response('', { status: 503 }); + return new Response('', { status: 503 }) } } -/** - * 离线提示页面 - */ -function createOfflinePage() { - return new Response( - ` +// ============================================ +// 离线页面 +// ============================================ + +function offlinePage() { + return new Response(`
- -SearchGal 是一个在线搜索服务,
请连接网络后使用
SearchGal 是在线搜索服务,请连接网络后使用
-请检查你的网络是否正常连接
+检查网络是否正常