/* * wait4x.online Service Worker — 静态站保守策略 * 设计约束(改前必读): * - HTML 走 network-first:站点是 SSG 但内容会更新,且上游还有 ESA 边缘缓存—— * SW 不能比边缘更激进,否则"刷了缓存还有 SW 旧页"双重陈旧坑 * - /_astro/*(内容哈希文件名)、字体切片、photos 为不可变资源:cache-first * - Artalk 评论 API 与 /admin 完全绕过 SW(动态数据不缓存、CMS 走网络) * - 无 precache(首访零负担);激活时清旧版本缓存 */ const VERSION = 'w4x-v2'; // install 预缓存核心路由(SSG 全量 ~30 页 HTML,<1MB,换来全站离线可读; // 列表由构建产物路由手工维护,新增顶层页时补一行) // PRECACHE:AUTO-BEGIN 以下为兜底清单——构建期由 scripts/build-perf/gen-sw-precache.mjs // 按产物自动重写(规则:顶层+en 轨路由,深度≤1 段;/admin 永不预缓存) const PRECACHE = [ '/', '/archives/', '/gallery/', '/moments/', '/books/', '/opensource/', '/about/', '/terminal/', '/en/', '/en/archives/', '/en/gallery/', '/en/moments/', '/en/books/', '/en/opensource/', '/en/about/', '/en/terminal/', ]; // PRECACHE:AUTO-END const IMMUTABLE = [ /\/_astro\//, // 构建产物(文件名带哈希) /\/fonts?\//, // 字体切片(fontsource,内容不变) /\/photos\//, // 图库压缩版(git 仓管理,变更=换文件) /\/icons\//, // PWA 图标 /\.(?:woff2|png|jpg|webp|svg|ico|css|js|webmanifest|xml)$/i, // 兜底静态后缀 ]; const NEVER_CACHE = [/\/api\//, /\/artalk/, /\/admin/, /\/wp-/]; self.addEventListener('install', (e) => { e.waitUntil( (async () => { const cache = await caches.open(VERSION); const results = await Promise.allSettled(PRECACHE.map((u) => cache.add(new Request(u, { cache: 'reload' })))); // 探针:install 失败原因必须可见(allSettled 静默是调试黑洞) results.forEach((r, i) => { if (r.status === 'rejected') console.error(`[sw-install] ${PRECACHE[i]} →`, r.reason); }); console.log(`[sw-install] done ok=${results.filter(r => r.status === 'fulfilled').length}/${PRECACHE.length} keys=${(await cache.keys()).length}`); self.skipWaiting(); })(), ); }); self.addEventListener('activate', (e) => { e.waitUntil( (async () => { const keys = await caches.keys(); await Promise.all(keys.filter((k) => k !== VERSION).map((k) => caches.delete(k))); await self.clients.claim(); })(), ); }); self.addEventListener('fetch', (e) => { const req = e.request; if (req.method !== 'GET') return; const url = new URL(req.url); if (url.origin !== self.location.origin) return; // 评论等三方资源不掺和 if (NEVER_CACHE.some((re) => re.test(url.pathname))) return; if (req.mode === 'navigate') { // HTML network-first:网络失败时回缓存(离线阅读兜底),成功时后台更新缓存 e.respondWith( (async () => { const cache = await caches.open(VERSION); try { const fresh = await fetch(req); if (fresh.ok) cache.put(req, fresh.clone()); return fresh; } catch { const hit = await cache.match(req); return hit || Response.error(); } })(), ); return; } if (IMMUTABLE.some((re) => re.test(url.pathname))) { e.respondWith( (async () => { const cache = await caches.open(VERSION); const hit = await cache.match(req); if (hit) return hit; const res = await fetch(req); if (res.ok && res.status === 200) cache.put(req, res.clone()); return res; })(), ); } });