mcpField
W
web-pwa-service-workers

web-pwa-service-workers

github.com/agents-inc开发2698 次安装2 天前更新
代码仓库

关于 web-pwa-service-workers

为使用 Service Worker 的 Web 开发者整理生命周期、缓存与更新模式,强调请求失败兜底和由用户控制的版本切换。

主要功能

  1. 01Service Worker 生命周期处理
  2. 02按内容选择缓存策略
  3. 03请求路由与离线兜底
  4. 04缓存版本清理
  5. 05检测并应用更新

适用场景

  1. 01为应用 Shell 预缓存资源
  2. 02为导航请求配置网络优先
  3. 03网络不可用时显示离线页面
  4. 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.yaml
reference.md
examples/
caching.md
core.md
updates.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” 会跳过等待阶段,因此更新处理在开发环境中看似正常,在生产环境中却会失败
  • 不透明的跨域响应会按远高于实际大小的填充后大小计入存储配额
  • 未设置超时的网络优先策略,在连接实际上仍然可用时会无限期挂起

评论

支持 Markdown