service-work.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. const cacheName = 'spa-cache-v1';
  2. // —— Cache pressure controls ——
  3. const MAX_NOHASH_ENTRIES = 500; // 每个无 hash 版本缓存的最大条目数
  4. const MAX_HASH_ENTRIES = 200; // 带 hash 静态资源缓存最大条目数
  5. const MAX_IMG_RESPONSE_SIZE = 1_500_000; // 超过此大小(字节)不入缓存
  6. // iOS/Safari 下调限额(小容量设备)
  7. const IS_SAFARI = /Safari\//.test(self.navigator.userAgent) && !/Chrome\//.test(self.navigator.userAgent);
  8. const IS_FB_BROWSER = /FBAN|FBAV|FB_IAB/i.test(self.navigator.userAgent);
  9. const IOS_LIMIT_FACTOR = IS_SAFARI ? 0.5 : 1;
  10. const FB_LIMIT_FACTOR = IS_FB_BROWSER ? 0.3 : 1;
  11. const DEVICE_FACTOR = Math.min(IOS_LIMIT_FACTOR, FB_LIMIT_FACTOR);
  12. // —— Image cache configs ——
  13. const IMG_NOHASH_CACHE_PREFIX = 'img-webp-nohash-v'; // versioned
  14. const IMG_HASH_CACHE_NAME = 'img-webp-hash'; // stable (hashed filenames)
  15. let IMG_VERSION = '0';
  16. async function readAppVersion() {
  17. try {
  18. const res = await fetch('/app_version.json', { cache: 'no-store' });
  19. if (res && res.ok) {
  20. const data = await res.json();
  21. IMG_VERSION = String(data?.version || '0');
  22. }
  23. } catch (e) {
  24. // ignore
  25. }
  26. }
  27. // —— Cookie Store 安全封装(iOS Safari 可能不支持) ——
  28. function hasCookieStore() {
  29. return 'cookieStore' in self && self.cookieStore && typeof self.cookieStore.get === 'function';
  30. }
  31. async function safeGetCookie(name) {
  32. if (!hasCookieStore()) return undefined;
  33. try {
  34. const res = await self.cookieStore.get(name);
  35. return res?.value;
  36. } catch (e) {
  37. return undefined;
  38. }
  39. }
  40. async function safeSetCookie(name, value) {
  41. if (!hasCookieStore()) return;
  42. try {
  43. await self.cookieStore.set(name, value);
  44. } catch (e) {
  45. // ignore
  46. }
  47. }
  48. async function safeDeleteCookie(name) {
  49. if (!hasCookieStore()) return;
  50. try {
  51. await self.cookieStore.delete(name);
  52. } catch (e) {
  53. // ignore
  54. }
  55. }
  56. // 定义主域名和备用域名列表
  57. var BACKUP_DOMAINS = ['https://winus777.com'];
  58. // 当前选定的域名
  59. let activeDomain = '';
  60. var ORIGINAL_DOMAIN;
  61. // 安装 Service Worker 并缓存 index.html
  62. self.addEventListener('install', function (e) {
  63. e.waitUntil(
  64. (async () => {
  65. await readAppVersion();
  66. const cache = await caches.open(cacheName);
  67. let ok;
  68. try {
  69. ORIGINAL_DOMAIN = await safeGetCookie('ORIGINAL_DOMAIN');
  70. ok = await cache.addAll([
  71. // '/index.html'
  72. ]);
  73. } catch (err) {
  74. console.log('err: cache.addAll');
  75. }
  76. return ok;
  77. })()
  78. );
  79. console.log('[Service Worker] Install');
  80. self.skipWaiting();
  81. });
  82. self.addEventListener('activate', e => {
  83. // Remove unwanted caches
  84. e.waitUntil(
  85. (async () => {
  86. await readAppVersion();
  87. ORIGINAL_DOMAIN = await safeGetCookie('ORIGINAL_DOMAIN');
  88. const names = await caches.keys();
  89. const validNoHashName = IMG_NOHASH_CACHE_PREFIX + IMG_VERSION;
  90. await Promise.all(
  91. names.map(name => {
  92. const isOldNoHash = name.startsWith(IMG_NOHASH_CACHE_PREFIX) && name !== validNoHashName;
  93. const isOldSpa = name !== cacheName && !name.startsWith(IMG_NOHASH_CACHE_PREFIX) && name !== IMG_HASH_CACHE_NAME;
  94. if (isOldNoHash || isOldSpa) {
  95. console.log('Service Worker: Clearing Old Cache', name);
  96. return caches.delete(name);
  97. }
  98. })
  99. );
  100. await self.clients.claim();
  101. })()
  102. );
  103. });
  104. self.addEventListener('fetch', event => {
  105. const request = event.request;
  106. const url = new URL(request.url);
  107. // 放行 Adjust
  108. if (url.origin === "https://web.adjust.com") {
  109. return;
  110. }
  111. // 仅处理同源请求
  112. const isSameOrigin = url.origin === self.location.origin;
  113. // 1) 导航请求:沿用现有逻辑
  114. if (request.mode === 'navigate') {
  115. event.respondWith(handleRequest(request, url));
  116. return;
  117. }
  118. // 2) 仅处理 .webp 图片
  119. const isWebpImage = request.destination === 'image' && /\.webp(\?|$)/.test(url.pathname);
  120. if (!isWebpImage || !isSameOrigin) {
  121. return; // 放过其余请求
  122. }
  123. const isNoHashDir = url.pathname.startsWith('/game_data/') || url.pathname.startsWith('/img/');
  124. // 2.1 无 hash 目录:Stale-While-Revalidate,版本化缓存名
  125. if (isNoHashDir) {
  126. const noHashCacheName = IMG_NOHASH_CACHE_PREFIX + IMG_VERSION;
  127. event.respondWith(
  128. (async () => {
  129. const cache = await caches.open(noHashCacheName);
  130. const cached = await cache.match(request);
  131. const networkPromise = fetch(request)
  132. .then(resp => {
  133. if (resp && resp.ok) {
  134. const len = Number(resp.headers.get('content-length') || '0');
  135. if (len === 0 || len <= MAX_IMG_RESPONSE_SIZE) {
  136. cache.put(request, resp.clone());
  137. // 限制缓存条数,删除最早的
  138. enforceCacheLimit(noHashCacheName, Math.floor(MAX_NOHASH_ENTRIES * DEVICE_FACTOR));
  139. }
  140. }
  141. return resp;
  142. })
  143. .catch(() => cached); // 网络失败时回退缓存
  144. if (cached) {
  145. event.waitUntil(networkPromise); // 背景更新
  146. return cached;
  147. }
  148. return networkPromise;
  149. })()
  150. );
  151. return;
  152. }
  153. // 2.2 其他(带 hash):Cache-First
  154. event.respondWith(
  155. (async () => {
  156. const cache = await caches.open(IMG_HASH_CACHE_NAME);
  157. const cached = await cache.match(request);
  158. if (cached) return cached;
  159. const resp = await fetch(request);
  160. if (resp && resp.ok) {
  161. const len = Number(resp.headers.get('content-length') || '0');
  162. if (len === 0 || len <= MAX_IMG_RESPONSE_SIZE) {
  163. cache.put(request, resp.clone());
  164. enforceCacheLimit(IMG_HASH_CACHE_NAME, Math.floor(MAX_HASH_ENTRIES * DEVICE_FACTOR));
  165. }
  166. }
  167. return resp;
  168. })()
  169. );
  170. });
  171. async function handleRequest(request, url) {
  172. const cachedResponse = await caches.match(request);
  173. const originalDomain = await safeGetCookie('ORIGINAL_DOMAIN');
  174. try {
  175. // 如果缓存中有匹配,则继续请求网络,并更新缓存
  176. return await fetchAndUpdateCache(request, originalDomain, url);
  177. } catch (err) {
  178. if (cachedResponse) {
  179. return cachedResponse;
  180. }
  181. try {
  182. return await fetch(request);
  183. } catch (e2) {
  184. return new Response('Service Worker Error', { status: 500 });
  185. }
  186. }
  187. }
  188. async function enforceCacheLimit(cacheName, maxEntries) {
  189. try {
  190. const cache = await caches.open(cacheName);
  191. const keys = await cache.keys();
  192. const over = keys.length - maxEntries;
  193. if (over > 0) {
  194. for (let i = 0; i < over; i++) {
  195. await cache.delete(keys[i]); // 近似 LRU:按插入顺序清理最早的
  196. }
  197. }
  198. } catch (e) {
  199. // ignore
  200. }
  201. }
  202. async function fetchAndUpdateCache(request, ORIGINAL_DOMAIN, url) {
  203. try {
  204. const networkResponse = await fetch(request);
  205. if (ORIGINAL_DOMAIN && ORIGINAL_DOMAIN !== url.origin && url.origin === self.location.origin) {
  206. await safeDeleteCookie('ORIGINAL_DOMAIN');
  207. }
  208. return networkResponse;
  209. } catch (err) {
  210. return await handleBackupDomain(request, ORIGINAL_DOMAIN, url);
  211. }
  212. }
  213. async function handleBackupDomain(request, ORIGINAL_DOMAIN, url) {
  214. if (ORIGINAL_DOMAIN && ORIGINAL_DOMAIN !== url.origin && url.origin === self.location.origin) {
  215. const backupResponse = await judgeActiveDomain(ORIGINAL_DOMAIN, request);
  216. if (backupResponse) return backupResponse;
  217. }
  218. return await fetchFromBackup(request);
  219. }
  220. async function fetchFromBackup(request) {
  221. for (let domain of BACKUP_DOMAINS) {
  222. const response = await judgeActiveDomain(domain, request);
  223. if (response) return response;
  224. }
  225. // return await fetchFromLatestBackupAddress(request);
  226. }
  227. async function judgeActiveDomain(url, originalRequest) {
  228. try {
  229. const newUrl = originalRequest.url.replace(self.location.origin, url);
  230. const newRequest = new Request(newUrl);
  231. const response = await fetch(newRequest);
  232. if (response.ok) {
  233. // 成功从备用域名获取资源,更新 activeDomain,将请求的html 存入缓存
  234. await safeSetCookie('ORIGINAL_DOMAIN', url);
  235. // 刷新界面
  236. return response.text().then(body => {
  237. // 定义路径前缀映射表
  238. const pathReplacements = {
  239. '/js': `${url}/js`,
  240. '/css': `${url}/css`,
  241. '/favicon': `${url}/favicon`,
  242. '/cdn': `${url}/cdn`,
  243. '/img': `${url}/img`,
  244. '/manifest': `${url}/product/manifest`
  245. };
  246. // 动态替换规则
  247. let newBody = body;
  248. for (const [prefix, replacement] of Object.entries(pathReplacements)) {
  249. const regex = new RegExp(`(src|href)="${prefix}`, 'g');
  250. newBody = newBody.replace(regex, `$1="${replacement}`);
  251. }
  252. newBody = newBody.replace(/src="\.\//g, `src="${url}/`);
  253. newBody = newBody.replace(/href="\.\//g, `href="${url}/`);
  254. return new Response(newBody, {
  255. headers: response.headers
  256. });
  257. });
  258. } else {
  259. console.log(`Backup domain ${url} returned status: ${response.status}`);
  260. }
  261. } catch (error) {
  262. console.log(`Backup domain ${url} failed:`, error);
  263. }
  264. }
  265. self.addEventListener('push', function (event) {
  266. if (event.data) {
  267. var notificationData = event.data.json();
  268. var title = notificationData.title;
  269. var imageSmall = notificationData.imageSmall;
  270. var imageLarge = notificationData.imageLarge;
  271. // 跳转地址
  272. var url = notificationData.url;
  273. var actions = notificationData.actions;
  274. var options = {
  275. body: notificationData.body || notificationData.message,
  276. icon: imageSmall,
  277. image: imageLarge,
  278. badge: imageSmall,
  279. actions: actions,
  280. data: {
  281. url: url
  282. }
  283. };
  284. event.waitUntil(self.registration.showNotification(title, options));
  285. }
  286. });
  287. // 处理点击消息
  288. self.addEventListener('notificationclick', function (event) {
  289. event.notification.close(); // 关闭推送通知
  290. // 获取推送通知的数据
  291. var notificationData = event.notification.data;
  292. // 获取要跳转的页面链接
  293. var targetPage = notificationData.url; // 假设推送通知的数据中包含了要跳转的页面链接
  294. // 执行页面跳转操作
  295. event.waitUntil(clients.openWindow(targetPage));
  296. });