- 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
94 lines
3.7 KiB
JavaScript
94 lines
3.7 KiB
JavaScript
/*
|
||
* 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;
|
||
})(),
|
||
);
|
||
}
|
||
});
|