Files
blog/public/sw.js
T
wait4xandGLM-5.3 3941a34f04 feat(pwa): SW 预缓存自动生成 + Sveltia 瞬间管理(池 4/5)
- gen-sw-precache.mjs post-build:按 dist 产物推导路由重写 dist/sw.js
  预缓存段(AUTO 标记锚定;规则=顶层+/en/ 轨深度≤1、/admin 永不入
  列)——新增顶层页自动进离线包,根治手工清单漏页维护债;实测 19
  路由含 moments 双轨、admin 排除
- build.sh 守卫挂钩(守卫先行纪律);sw.js 源内兜底清单保留
- Sveltia 加 moments 集合:随手发瞬间(text/image 可选+hint 自查条
  件必填、pubDate 日期件、tags、slug 日期模板)——管理后台从只管
  照片扩到内容面第一步

验证: 产物 sw 19 路由断言全过;E2E 33/33 PASS;yaml 解析过;bash -n 过

Spec: none (离线包自动化+后台扩容,车道级交付)

Co-authored-by: GLM-5.3
2026-10-26 20:02:32 +08:00

94 lines
3.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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;
})(),
);
}
});