| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- 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));
- });
|