From 5e408eeceb051b92ce977364b5eb37e7418ab9f4 Mon Sep 17 00:00:00 2001 From: AdingApkgg Date: Sat, 27 Dec 2025 03:17:56 +0800 Subject: [PATCH] Refactor service worker implementation and update caching strategies - Simplified service worker code in sw.js by consolidating caching patterns and improving resource handling. - Enhanced fetch event logic to streamline caching strategies for static resources and cross-origin requests. - Updated service worker registration and update handling in main.ts for better version management and user experience. - Improved offline page design and functionality for clearer user communication during network issues. - Adjusted font family order in SettingsModal.vue for consistency in code editor styling. --- public/sw.js | 363 +++++++++++-------------------- src/components/SettingsModal.vue | 2 +- src/main.ts | 129 +++-------- 3 files changed, 161 insertions(+), 333 deletions(-) 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 -
-
🌐
+
+
🌐

需要网络连接

-

SearchGal 是一个在线搜索服务,
请连接网络后使用

+

SearchGal 是在线搜索服务,请连接网络后使用

-

请检查你的网络是否正常连接

+

检查网络是否正常

-`, - { - status: 503, - statusText: 'Offline', - headers: { 'Content-Type': 'text/html; charset=utf-8' }, - } - ); +`, { + status: 503, + headers: { 'Content-Type': 'text/html;charset=utf-8' } + }) } -console.log(`[SW] Service Worker loaded, version: ${SW_VERSION}`); +console.log(`[SW] Loaded v${SW_VERSION}`) diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue index 473c886..13752e6 100644 --- a/src/components/SettingsModal.vue +++ b/src/components/SettingsModal.vue @@ -793,7 +793,7 @@ function resetAdvancedApiSettings(playSound = true) { /* IDE 风格代码编辑器 */ .code-editor { - font-family: "JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace !important; + font-family: Consolas, "Monaco", "JetBrains Mono", "Fira Code", monospace !important; font-size: 13px !important; line-height: 1.5 !important; min-height: 240px !important; diff --git a/src/main.ts b/src/main.ts index 70b22e1..f8d991c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -55,117 +55,54 @@ createProgressFetch() app.mount('#app') -// Service Worker 更新检测 -// 当前激活的 SW 版本(运行时获取) -let activatedSwVersion: string | null = null +// ============================================ +// Service Worker 注册与更新 +// ============================================ -// 获取 SW 版本 -async function getSwVersion(sw: ServiceWorker): Promise { - return new Promise((resolve) => { - const messageChannel = new MessageChannel() - messageChannel.port1.onmessage = (event) => { - resolve(event.data?.version || null) - } - sw.postMessage({ type: 'GET_VERSION' }, [messageChannel.port2]) - - // 超时处理 - setTimeout(() => resolve(null), 2000) - }) -} - -// 检查是否有新版本 -async function checkForNewVersion(registration: ServiceWorkerRegistration): Promise { - const sw = registration.active - if (!sw) {return false} - - const currentVersion = await getSwVersion(sw) - - // 首次获取版本时记录 - if (!activatedSwVersion && currentVersion) { - activatedSwVersion = currentVersion - console.log(`[SW] Current version: ${activatedSwVersion}`) - return false - } - - // 版本不同则有更新 - if (currentVersion && activatedSwVersion && currentVersion !== activatedSwVersion) { - console.log(`[SW] New version available: ${currentVersion} (was: ${activatedSwVersion})`) - return true - } - - return false -} - -// 触发 SW 更新 -function triggerSwUpdate(registration: ServiceWorkerRegistration) { - const waitingSw = registration.waiting - if (waitingSw) { - waitingSw.postMessage({ type: 'SKIP_WAITING' }) - } - // 刷新页面 - window.location.reload() -} - -// 自动应用更新(不显示弹窗) -function autoApplyUpdate(registration: ServiceWorkerRegistration) { - console.log('[SW] Auto-applying update...') - triggerSwUpdate(registration) -} - -// 注册 Service Worker (PWA 支持) if ('serviceWorker' in navigator) { window.addEventListener('load', async () => { try { - const registration = await navigator.serviceWorker.register('/sw.js') + const reg = await navigator.serviceWorker.register('/sw.js') + console.log('[SW] Registered') - // 检查更新 - registration.addEventListener('updatefound', () => { - const newWorker = registration.installing - if (newWorker) { - newWorker.addEventListener('statechange', () => { - if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { - // 有新版本可用,自动更新 - autoApplyUpdate(registration) - } - }) + // 新版本检测 + reg.addEventListener('updatefound', () => { + const worker = reg.installing + if (!worker) { + return } + + worker.addEventListener('statechange', () => { + // 新 SW 安装完成且有旧 SW 控制页面 = 有更新 + if (worker.state === 'installed' && navigator.serviceWorker.controller) { + console.log('[SW] Update available, activating...') + worker.postMessage({ type: 'SKIP_WAITING' }) + } + }) }) - // 首次获取当前版本 - if (registration.active) { - activatedSwVersion = await getSwVersion(registration.active) - console.log(`[SW] Registered, version: ${activatedSwVersion || 'unknown'}`) - } - - // 定期检查版本(每 5 分钟) - setInterval(async () => { - try { - await registration.update() - const hasNewVersion = await checkForNewVersion(registration) - if (hasNewVersion) { - autoApplyUpdate(registration) - } - } catch { - // 静默处理检查失败 + // 新 SW 激活后刷新页面 + let refreshing = false + navigator.serviceWorker.addEventListener('controllerchange', () => { + if (refreshing) { + return } - }, 5 * 60 * 1000) + refreshing = true + console.log('[SW] New version activated, reloading...') + window.location.reload() + }) + + // 定期检查更新(5 分钟) + setInterval(() => reg.update().catch(() => {}), 5 * 60 * 1000) // 页面可见时检查更新 - document.addEventListener('visibilitychange', async () => { + document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { - try { - await registration.update() - } catch { - // 静默处理 - } + reg.update().catch(() => {}) } }) - } catch { - // 静默处理注册失败 + // 静默处理 } }) } - -// 导出更新函数供组件使用 -;(window as Window & { triggerSwUpdate?: typeof triggerSwUpdate }).triggerSwUpdate = triggerSwUpdate