const cacheName = 'spa-cache-v1'; // —— Cache pressure controls —— const MAX_NOHASH_ENTRIES = 500; // 每个无 hash 版本缓存的最大条目数 const MAX_HASH_ENTRIES = 200; // 带 hash 静态资源缓存最大条目数 const MAX_IMG_RESPONSE_SIZE = 1_500_000; // 超过此大小(字节)不入缓存 // iOS/Safari 下调限额(小容量设备) const IS_SAFARI = /Safari\//.test(self.navigator.userAgent) && !/Chrome\//.test(self.navigator.userAgent); const IS_FB_BROWSER = /FBAN|FBAV|FB_IAB/i.test(self.navigator.userAgent); const IOS_LIMIT_FACTOR = IS_SAFARI ? 0.5 : 1; const FB_LIMIT_FACTOR = IS_FB_BROWSER ? 0.3 : 1; const DEVICE_FACTOR = Math.min(IOS_LIMIT_FACTOR, FB_LIMIT_FACTOR); // —— Image cache configs —— const IMG_NOHASH_CACHE_PREFIX = 'img-webp-nohash-v'; // versioned const IMG_HASH_CACHE_NAME = 'img-webp-hash'; // stable (hashed filenames) let IMG_VERSION = '0'; async function readAppVersion() { try { const res = await fetch('/app_version.json', { cache: 'no-store' }); if (res && res.ok) { const data = await res.json(); IMG_VERSION = String(data?.version || '0'); } } catch (e) { // ignore } } // —— Cookie Store 安全封装(iOS Safari 可能不支持) —— function hasCookieStore() { return 'cookieStore' in self && self.cookieStore && typeof self.cookieStore.get === 'function'; } async function safeGetCookie(name) { if (!hasCookieStore()) return undefined; try { const res = await self.cookieStore.get(name); return res?.value; } catch (e) { return undefined; } } async function safeSetCookie(name, value) { if (!hasCookieStore()) return; try { await self.cookieStore.set(name, value); } catch (e) { // ignore } } async function safeDeleteCookie(name) { if (!hasCookieStore()) return; try { await self.cookieStore.delete(name); } catch (e) { // ignore } } // 定义主域名和备用域名列表 var BACKUP_DOMAINS = ['https://winus777.com']; // 当前选定的域名 let activeDomain = ''; var ORIGINAL_DOMAIN; // 安装 Service Worker 并缓存 index.html self.addEventListener('install', function (e) { e.waitUntil( (async () => { await readAppVersion(); const cache = await caches.open(cacheName); let ok; try { ORIGINAL_DOMAIN = await safeGetCookie('ORIGINAL_DOMAIN'); ok = await cache.addAll([ // '/index.html' ]); } catch (err) { console.log('err: cache.addAll'); } return ok; })() ); console.log('[Service Worker] Install'); self.skipWaiting(); }); self.addEventListener('activate', e => { // Remove unwanted caches e.waitUntil( (async () => { await readAppVersion(); ORIGINAL_DOMAIN = await safeGetCookie('ORIGINAL_DOMAIN'); const names = await caches.keys(); const validNoHashName = IMG_NOHASH_CACHE_PREFIX + IMG_VERSION; await Promise.all( names.map(name => { const isOldNoHash = name.startsWith(IMG_NOHASH_CACHE_PREFIX) && name !== validNoHashName; const isOldSpa = name !== cacheName && !name.startsWith(IMG_NOHASH_CACHE_PREFIX) && name !== IMG_HASH_CACHE_NAME; if (isOldNoHash || isOldSpa) { console.log('Service Worker: Clearing Old Cache', name); return caches.delete(name); } }) ); await self.clients.claim(); })() ); }); self.addEventListener('fetch', event => { const request = event.request; const url = new URL(request.url); // 仅处理同源请求 const isSameOrigin = url.origin === self.location.origin; // 1) 导航请求:沿用现有逻辑 if (request.mode === 'navigate') { event.respondWith(handleRequest(request, url)); return; } // 2) 仅处理 .webp 图片 const isWebpImage = request.destination === 'image' && /\.webp(\?|$)/.test(url.pathname); if (!isWebpImage || !isSameOrigin) { return; // 放过其余请求 } const isNoHashDir = url.pathname.startsWith('/game_data/') || url.pathname.startsWith('/img/'); // 2.1 无 hash 目录:Stale-While-Revalidate,版本化缓存名 if (isNoHashDir) { const noHashCacheName = IMG_NOHASH_CACHE_PREFIX + IMG_VERSION; event.respondWith( (async () => { const cache = await caches.open(noHashCacheName); const cached = await cache.match(request); const networkPromise = fetch(request) .then(resp => { if (resp && resp.ok) { const len = Number(resp.headers.get('content-length') || '0'); if (len === 0 || len <= MAX_IMG_RESPONSE_SIZE) { cache.put(request, resp.clone()); // 限制缓存条数,删除最早的 enforceCacheLimit(noHashCacheName, Math.floor(MAX_NOHASH_ENTRIES * DEVICE_FACTOR)); } } return resp; }) .catch(() => cached); // 网络失败时回退缓存 if (cached) { event.waitUntil(networkPromise); // 背景更新 return cached; } return networkPromise; })() ); return; } // 2.2 其他(带 hash):Cache-First event.respondWith( (async () => { const cache = await caches.open(IMG_HASH_CACHE_NAME); const cached = await cache.match(request); if (cached) return cached; const resp = await fetch(request); if (resp && resp.ok) { const len = Number(resp.headers.get('content-length') || '0'); if (len === 0 || len <= MAX_IMG_RESPONSE_SIZE) { cache.put(request, resp.clone()); enforceCacheLimit(IMG_HASH_CACHE_NAME, Math.floor(MAX_HASH_ENTRIES * DEVICE_FACTOR)); } } return resp; })() ); }); async function handleRequest(request, url) { const cachedResponse = await caches.match(request); const originalDomain = await safeGetCookie('ORIGINAL_DOMAIN'); try { // 如果缓存中有匹配,则继续请求网络,并更新缓存 return await fetchAndUpdateCache(request, originalDomain, url); } catch (err) { if (cachedResponse) { return cachedResponse; } try { return await fetch(request); } catch (e2) { return new Response('Service Worker Error', { status: 500 }); } } } async function enforceCacheLimit(cacheName, maxEntries) { try { const cache = await caches.open(cacheName); const keys = await cache.keys(); const over = keys.length - maxEntries; if (over > 0) { for (let i = 0; i < over; i++) { await cache.delete(keys[i]); // 近似 LRU:按插入顺序清理最早的 } } } catch (e) { // ignore } } async function fetchAndUpdateCache(request, ORIGINAL_DOMAIN, url) { try { const networkResponse = await fetch(request); if (ORIGINAL_DOMAIN && ORIGINAL_DOMAIN !== url.origin && url.origin === self.location.origin) { await safeDeleteCookie('ORIGINAL_DOMAIN'); } return networkResponse; } catch (err) { return await handleBackupDomain(request, ORIGINAL_DOMAIN, url); } } async function handleBackupDomain(request, ORIGINAL_DOMAIN, url) { if (ORIGINAL_DOMAIN && ORIGINAL_DOMAIN !== url.origin && url.origin === self.location.origin) { const backupResponse = await judgeActiveDomain(ORIGINAL_DOMAIN, request); if (backupResponse) return backupResponse; } return await fetchFromBackup(request); } async function fetchFromBackup(request) { for (let domain of BACKUP_DOMAINS) { const response = await judgeActiveDomain(domain, request); if (response) return response; } // return await fetchFromLatestBackupAddress(request); } async function judgeActiveDomain(url, originalRequest) { try { const newUrl = originalRequest.url.replace(self.location.origin, url); const newRequest = new Request(newUrl); const response = await fetch(newRequest); if (response.ok) { // 成功从备用域名获取资源,更新 activeDomain,将请求的html 存入缓存 await safeSetCookie('ORIGINAL_DOMAIN', url); // 刷新界面 return response.text().then(body => { // 定义路径前缀映射表 const pathReplacements = { '/js': `${url}/js`, '/css': `${url}/css`, '/favicon': `${url}/favicon`, '/cdn': `${url}/cdn`, '/img': `${url}/img`, '/manifest': `${url}/product/manifest` }; // 动态替换规则 let newBody = body; for (const [prefix, replacement] of Object.entries(pathReplacements)) { const regex = new RegExp(`(src|href)="${prefix}`, 'g'); newBody = newBody.replace(regex, `$1="${replacement}`); } newBody = newBody.replace(/src="\.\//g, `src="${url}/`); newBody = newBody.replace(/href="\.\//g, `href="${url}/`); return new Response(newBody, { headers: response.headers }); }); } else { console.log(`Backup domain ${url} returned status: ${response.status}`); } } catch (error) { console.log(`Backup domain ${url} failed:`, error); } } self.addEventListener('push', function (event) { if (event.data) { var notificationData = event.data.json(); var title = notificationData.title; var imageSmall = notificationData.imageSmall; var imageLarge = notificationData.imageLarge; // 跳转地址 var url = notificationData.url; var actions = notificationData.actions; var options = { body: notificationData.body || notificationData.message, icon: imageSmall, image: imageLarge, badge: imageSmall, actions: actions, data: { url: url } }; event.waitUntil(self.registration.showNotification(title, options)); } }); // 处理点击消息 self.addEventListener('notificationclick', function (event) { event.notification.close(); // 关闭推送通知 // 获取推送通知的数据 var notificationData = event.notification.data; // 获取要跳转的页面链接 var targetPage = notificationData.url; // 假设推送通知的数据中包含了要跳转的页面链接 // 执行页面跳转操作 event.waitUntil(clients.openWindow(targetPage)); });