关于 web-pwa-service-workers
为使用 Service Worker 的 Web 开发者整理生命周期、缓存与更新模式,强调请求失败兜底和由用户控制的版本切换。
主要功能
- 01Service Worker 生命周期处理
- 02按内容选择缓存策略
- 03请求路由与离线兜底
- 04缓存版本清理
- 05检测并应用更新
适用场景
- 01为应用 Shell 预缓存资源
- 02为导航请求配置网络优先
- 03网络不可用时显示离线页面
- 04提示用户切换到等待中的 Worker
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-pwa-service-workers安装后在 Claude Code 中输入/web-pwa-service-workers确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/caching.mdcore.mdupdates.md详细介绍
Service Worker 模式
**快速指南:**Service Worker 是一个可编程的网络代理,有自己的生命周期: 安装阶段预缓存,激活阶段清理,fetch 阶段拦截。根据内容选择缓存策略——带哈希的资源采用缓存优先,HTML 采用网络优先,非关键 API 读取采用 stale-while-revalidate。大多数错误都由两个细节引起:响应正文只能读取一次,因此要使用
cache.put(request, response.clone());新 Worker 会一直等待,直到所有使用旧 Worker 的标签页都关闭,因此更新需要显式检测,并由用户触发skipWaiting。
详细资源:
- examples/core.md — 注册、完整的生命周期模板、四种策略的实现、消息类型
- examples/caching.md — 后台刷新、过期时间戳、按端点路由、导航预加载、配额清理
- examples/updates.md — 版本消息、激进式/延迟式/空闲时更新、渐进式发布、缓存迁移
- reference.md — 按内容类型划分的策略表、生命周期和 Cache API 速查、更新策略选择、审核清单
适用路径
- 预缓存应用外壳 — HTML 是一个缓存的文档,路由在客户端进行。采用缓存优先,并跳过导航预加载;没有内容需要预加载。
- 服务端渲染或经过身份验证的页面 — 每次导航都会加载一个新文档。采用带超时和离线回退的网络优先策略,并启用导航预加载,以便在 Worker 启动前开始请求。参阅 examples/caching.md 模式 8。
<critical_requirements>
编写 Service Worker 代码前
**将每个异步生命周期任务都包装在 event.waitUntil() 中。**它会让 Worker 保持运行,直到 Promise 完成;否则浏览器可以在预缓存过程中终止 Worker。
**每个缓存名称都加入版本号,并在 activate 期间删除非当前缓存。**这样可以确保升级过程干净,并避免存储空间无限增长。
**缓存前先克隆——cache.put(request, response.clone())。**响应正文只能读取一次,因此缓存原始响应会导致客户端收到空响应。
**由客户端决定何时让等待中的 Worker 接管。**检测等待中的 Worker,通知用户,并在收到用户消息后调用 skipWaiting(),这样公开会话期间的行为就不会发生变化。
**为每条 fetch 路径提供回退。**导航时使用预缓存的 offline.html,并将构造的 Response 作为最后手段,这样网络故障就会显示你编写的页面。
</critical_requirements>
自动检测: navigator.serviceWorker、serviceWorker.register、ServiceWorkerGlobalScope、 sw.js、sw.ts、self.skipWaiting、clients.claim、event.waitUntil、event.respondWith、 event.preloadResponse、caches.open、caches.match、cache.addAll、CacheStorage、navigationPreload、 updateViaCache、controllerchange、updatefound、precache
适用场景:
- 从 Worker 线程拦截并响应网络请求
- 为每种内容类型选择并实现缓存策略
- 预缓存应用外壳并清理已被取代的缓存
- 检测等待中的 Worker,并按用户的意愿应用更新
- 在缓存和网络都不可用时提供离线回退
其他部分处理:
- 应用拥有并写入的结构化本地数据——响应缓存保存的是服务器返回的内容,其生命周期不同于用户离线编辑的记录
- 将断开连接期间产生的变更排入队列,并在之后进行协调
- 推送通知的内容和权限流程——Worker 的
push事件是一个投递钩子,显示什么内容属于产品决策
## 设计理念
服务工作线程是代理,而不是插件:安装后,它会看到其作用域内的每个请求,而它无法响应的任何请求都会被它破坏。
生命周期机制旨在避免两个版本同时运行。新工作线程会立即安装,但会等待由旧工作线程控制的所有客户端关闭,这样同一个页面就不会一半运行旧资源、一半运行新资源。
Registration → Download → Install → Waiting → Activate → Fetch
↓ ↓
(skipWaiting) (claim)
skipWaiting() 和 clients.claim() 是绕过这项保证的两种方式,而且它们都需要主动选择,这是有原因的。只有在用户要求更新,或修复紧急到值得在会话中途更改时,才使用它们。
核心模式
模式 1:注册并检测更新
通过功能检测进行注册,定期检查更新,并跟踪等待中的工作线程,以便界面可以提供更新选项。
const registration = await navigator.serviceWorker.register("/sw.js", {
scope: "/",
updateViaCache: "none", // ask the server for the worker script every time
});
setInterval(() => registration.update(), UPDATE_CHECK_INTERVAL_MS);
registration.addEventListener("updatefound", () => {
const installing = registration.installing;
installing?.addEventListener("statechange", () => {
const isWaiting =
installing.state === "installed" && navigator.serviceWorker.controller;
if (isWaiting) notifyUpdateAvailable();
});
});
完整代码:examples/core.md
模式 2:生命周期处理程序
在 install 阶段预缓存,在 activate 阶段删除已被替代的缓存,并通过消息触发 skipWaiting,而不是无条件调用它。
self.addEventListener("install", (event: ExtendableEvent) => {
event.waitUntil(
caches.open(CACHES.static).then((cache) => cache.addAll(PRECACHE_URLS)),
);
});
self.addEventListener("activate", (event: ExtendableEvent) => {
event.waitUntil(
(async () => {
const names = await caches.keys();
const current: string[] = Object.values(CACHES);
await Promise.all(
names.filter((n) => !current.includes(n)).map((n) => caches.delete(n)),
);
await self.clients.claim();
})(),
);
});
self.addEventListener("message", (event: ExtendableMessageEvent) => {
if (event.data?.type === "SKIP_WAITING") self.skipWaiting();
});
完整代码:examples/core.md
模式 3:缓存策略
四种策略几乎可以涵盖所有情况。关于不同内容适用哪种策略,请参阅 reference.md。
| 策略 | 行为 |
|---|---|
| 缓存优先 | 提供缓存副本;若不可用,则回退到网络 |
| 网络优先 | 让网络请求与超时竞速,然后缓存 |
| 过期后重新验证 | 提供缓存副本,并在后台刷新 |
| 仅缓存 / 仅网络 | 单一来源,不进行回退 |
每种实现都会在缓存前检查 response.ok,在调用 cache.put 前克隆响应,并限制条目数量。
const networkResponse = await fetch(request);
if (networkResponse.ok) {
cache.put(request, networkResponse.clone()); // the clone is cached
}
return networkResponse; // the original goes to the client
完整代码:examples/core.md;过期机制和按端点区分的变体见 examples/caching.md
模式 4:Fetch 路由
根据请求特征而不是仅根据 URL 进行路由:request.mode、request.destination 和路径名分别回答不同的问题。不调用 respondWith 就返回,可让浏览器按常规方式处理请求。
self.addEventListener("fetch", (event: FetchEvent) => {
const { request } = event;
const url = new URL(request.url);
if (request.method !== "GET") return;
if (url.origin !== location.origin) return;
if (request.mode === "navigate") {
event.respondWith(networkFirst(request, CACHES.pages));
} else if (request.destination === "image") {
event.respondWith(cacheFirstWithLimit(request, CACHES.images, MAX_IMAGES));
} else if (url.pathname.startsWith("/api/")) {
event.respondWith(staleWhileRevalidate(request, CACHES.api));
} else {
event.respondWith(cacheFirst(request, CACHES.static));
}
});
完整代码:examples/core.md
模式 5:离线回退
在 install 阶段预缓存离线页面;当导航请求没有缓存条目且网络不可用时,提供该页面。将 Response 作为最后的备用方案,以确保不会出现最终显示浏览器自身错误页面的情况。
if (request.mode === "navigate") {
const offlinePage = await caches.match("/offline.html");
if (offlinePage) return offlinePage;
}
return new Response("<h1>Offline</h1><p>Check your connection.</p>", {
status: 503,
headers: { "Content-Type": "text/html" },
});
完整代码:examples/core.md
模式 6:导航预加载
工作线程启动大约需要 50ms(桌面设备)或 250ms 甚至更久(网络较慢的手机),而网络优先的导航请求在真正开始前就要等待这段时间。导航预加载会在工作线程启动的同时并行发起请求。
// activate
if (self.registration.navigationPreload) {
await self.registration.navigationPreload.enable();
}
// fetch
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
cache.put(event.request, preloadResponse.clone());
return preloadResponse;
}
启用后,应使用 event.preloadResponse;改为调用 fetch(event.request) 会针对同一资源发起两次网络请求。
完整代码:examples/caching.md
模式 7:由用户控制更新
向用户显示等待中的工作线程;用户接受后发送 SKIP_WAITING,并在 controllerchange 时重新加载一次,以确保页面和工作线程保持一致。
if (registration.waiting) showUpdateBanner();
function applyUpdate() {
registration.waiting?.postMessage({ type: "SKIP_WAITING" });
}
let refreshing = false;
navigator.serviceWorker.addEventListener("controllerchange", () => {
if (refreshing) return;
refreshing = true;
window.location.reload();
});
完整代码:examples/updates.md — 版本消息传递、延迟应用和空闲时应用、渐进式发布、缓存迁移
<red_flags>
风险信号
运行时会出错:
- 在 install 或 activate 中执行异步任务,却没有使用
event.waitUntil()——浏览器可能会在任务执行过程中终止 worker——请用 promise 将其包装起来 - 调用
cache.put(request, response)时没有使用.clone()——响应正文会被消耗,客户端将收不到任何内容——请缓存副本并返回原始响应 - 缓存时不检查
response.ok——404 或 500 响应会被存储,并在缓存的整个生命周期内返回——请在调用put前进行检查 - 缓存名称未进行版本控制——activate 无法区分当前缓存和已被取代的缓存——请插入版本常量
- 导航时没有
offline.html——用户会看到浏览器的错误页面——请在 install 时预缓存该页面,并在 fetch 出错时返回它 - 在启用了导航预加载的 worker 中调用
fetch(event.request)——每次导航都会发出两个网络请求——请使用event.preloadResponse - 尝试缓存 POST 请求——Cache API 的键基于 GET 请求——当
request.method !== "GET"时直接返回
容易造成意外的行为:
- 作用域取决于脚本的位置:
/sw.js控制/,而/scripts/sw.js只控制/scripts/ - Service Worker 需要 HTTPS,但开发时 localhost 除外
- 空闲的 worker 会被终止并重新启动,因此模块级状态不会在事件之间保留
clients.claim()无需重新加载即可接管控制权,因此已打开的页面会同时保留旧 HTML 和新 worker- 只有 worker 脚本的字节内容发生变化才会触发更新——编辑 Web 应用清单或预缓存资源不会触发更新
- DevTools 中的 “Update on reload” 会跳过等待阶段,因此更新处理在开发环境中看似正常,在生产环境中却会失败
- 不透明的跨域响应会按远高于实际大小的填充后大小计入存储配额
- 未设置超时的网络优先策略,在连接实际上仍然可用时会无限期挂起