Tips & Tricks (更新: 2026/6/2)

用 Claude Code 实现 Service Worker:缓存、更新与离线 UX

讲清 Service Worker、缓存失效、更新生命周期、离线 UX,并提供可复制运行的 Claude Code 示例。

用 Claude Code 实现 Service Worker:缓存、更新与离线 UX

Service Worker 是 PWA 和离线能力的核心,但它不是“加一个缓存就会变快”的开关。如果让 Claude Code 在没有边界的情况下“帮我加缓存”,应用可能会保留旧 HTML、缓存用户私有数据,或者上线新版本后迟迟不更新。

可以先把 Service Worker 理解成浏览器和服务器之间的小型代理。页面发起请求时,它可以决定走网络、走 Cache API,或在离线时返回一个准备好的 fallback 页面。本文会用 Claude Code 的工作方式整理实现前的决策、可复制运行的最小代码、缓存失效、更新生命周期、离线 UX,以及真实项目里常见的坑。

实现时建议同时打开官方资料:MDN Service Worker APIweb.dev Service Worker 指南web.dev 缓存建议、以及更工程化的 Chrome Workbox docs。相关内部阅读可以继续看 Claude Code PWA 指南Claude Code 缓存策略IndexedDB 实现指南

Service Worker 做什么

Service Worker 是运行在页面之外的 JavaScript。它不能直接操作 DOM,也不能直接改按钮、表单或 React state。它能做的是拦截符合条件的网络请求,并决定用网络响应、缓存响应,还是离线页面响应。

普通页面脚本会随着标签页关闭而结束;Service Worker 是事件驱动的后台脚本,浏览器会在 install、activate、fetch、push 等事件发生时唤醒它。对多数产品来说,第一阶段只要掌握 fetch、Cache API 和更新生命周期就足够了。

sequenceDiagram
  participant User as 用户
  participant Page as 页面
  participant SW as Service Worker
  participant Cache as Cache API
  participant Net as 服务器
  User->>Page: 打开站点
  Page->>SW: 注册 /sw.js
  Page->>SW: 发起 fetch 请求
  SW->>Cache: 检查缓存
  alt 有缓存
    Cache-->>SW: 返回已保存响应
  else 无缓存
    SW->>Net: 请求最新内容
    Net-->>SW: 返回响应
  end
  SW-->>Page: 页面渲染响应

所以它更像通信调度器,而不是性能魔法。质量取决于你决定缓存什么、何时删除、失败时给用户看什么。

适合的使用场景

场景价值注意点
文档站或技术博客文章、CSS、图片、字体在二次访问时更快HTML 缓存太久会挡住最新修正
SaaS 控制台弱网下仍能显示导航和基础框架不要缓存账单、账号、权限相关响应
外勤表单离线时保留下草稿或待发送任务POST 不放 Cache API,使用 IndexedDB 队列
电商或媒体目录缩略图和静态资源减少重复下载价格、库存、受保护图片必须控制新鲜度

Masa 在一个小型教材站测试过:只缓存图片和字体时,二次访问明显更轻;但把文章 HTML 也粗暴设成 Cache First 后,错字修正无法及时到达读者。给 Claude Code 的指令不要写“全部缓存”,而要写清每类资源的寿命和边界。

给 Claude Code 的清晰任务

Service Worker 的提示词要包含禁止事项和验证方法。下面的粒度更接近真实项目。

请给现有 Vite 应用增加 Service Worker。

要求:
- /sw.js 放在 public 根目录,scope 为 /
- 只缓存 GET 静态资源
- HTML 导航使用 Network First
- 离线导航失败时返回 /offline.html
- 不缓存 API、POST、认证页面、外部来源
- 缓存名包含日期或版本号
- activate 阶段删除旧缓存
- 新 worker 进入 waiting 时提示用户刷新

验证:
- 在 Chrome DevTools > Application > Service Workers 查看注册状态
- 切到 Offline 后刷新,确认出现 /offline.html
- 修改 CACHE_VERSION,确认旧缓存被删除

API、认证页面和 POST 的排除必须写明,因为 Service Worker 的 bug 很容易变成数据泄漏或旧数据问题。

可复制运行的最小实现

把下面四个文件放进空目录,例如 sw-demo,然后启动本地服务器。Service Worker 只在 HTTPS 或 localhost 下工作,直接双击 HTML 文件不会注册成功。

python -m http.server 5173

浏览器打开 http://localhost:5173

<!-- index.html -->
<!doctype html>
<html lang="zh">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Service Worker Demo</title>
    <style>
      body {
        font-family: system-ui, sans-serif;
        margin: 2rem;
        line-height: 1.7;
      }
      button {
        padding: 0.7rem 1rem;
      }
    </style>
  </head>
  <body>
    <h1>Service Worker Demo</h1>
    <p id="status">等待注册。</p>
    <button type="button" onclick="location.reload()">重新加载</button>
    <script src="/register-sw.js"></script>
  </body>
</html>
// register-sw.js
const status = document.querySelector("#status");
let reloadRequested = false;
let updatePromptShown = false;

function setStatus(message) {
  if (status) status.textContent = message;
}

function askToReload(worker) {
  if (updatePromptShown) return;
  updatePromptShown = true;

  const ok = window.confirm("发现新版本。现在重新加载吗?");

  if (ok) {
    reloadRequested = true;
    worker.postMessage({ type: "SKIP_WAITING" });
  }
}

async function registerServiceWorker() {
  if (!("serviceWorker" in navigator)) {
    setStatus("当前浏览器不支持 Service Worker。");
    return;
  }

  try {
    const registration = await navigator.serviceWorker.register("/sw.js", {
      scope: "/",
    });

    setStatus(`Service Worker 已注册: ${registration.scope}`);

    if (registration.waiting && navigator.serviceWorker.controller) {
      askToReload(registration.waiting);
    }

    registration.addEventListener("updatefound", () => {
      const worker = registration.installing;
      if (!worker) return;

      worker.addEventListener("statechange", () => {
        const hasOldController = Boolean(navigator.serviceWorker.controller);
        if (worker.state === "installed" && hasOldController) {
          askToReload(worker);
        }
      });
    });
  } catch (error) {
    console.error(error);
    setStatus("Service Worker 注册失败。");
  }
}

navigator.serviceWorker?.addEventListener("controllerchange", () => {
  if (!reloadRequested) return;
  window.location.reload();
});

registerServiceWorker();
// sw.js
const CACHE_VERSION = "2026-06-02-v1";
const CACHE_PREFIX = "claude-sw-demo";
const CACHE_NAME = `${CACHE_PREFIX}-${CACHE_VERSION}`;

const APP_SHELL = [
  "/",
  "/index.html",
  "/offline.html",
  "/register-sw.js",
];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)),
  );
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((names) =>
      Promise.all(
        names
          .filter((name) => name.startsWith(CACHE_PREFIX))
          .filter((name) => name !== CACHE_NAME)
          .map((name) => caches.delete(name)),
      ),
    ),
  );
  self.clients.claim();
});

self.addEventListener("message", (event) => {
  if (event.data?.type === "SKIP_WAITING") {
    self.skipWaiting();
  }
});

self.addEventListener("fetch", (event) => {
  const { request } = event;
  if (request.method !== "GET") return;

  const url = new URL(request.url);
  if (url.origin !== self.location.origin) return;

  if (request.mode === "navigate") {
    event.respondWith(networkFirstNavigation(request));
    return;
  }

  if (["style", "script", "font", "image"].includes(request.destination)) {
    event.respondWith(staleWhileRevalidate(request));
  }
});

async function networkFirstNavigation(request) {
  const cache = await caches.open(CACHE_NAME);

  try {
    const response = await fetch(request);
    if (response.ok) cache.put(request, response.clone());
    return response;
  } catch {
    return (
      (await cache.match(request)) ||
      (await cache.match("/offline.html")) ||
      new Response("Offline", { status: 503 })
    );
  }
}

async function staleWhileRevalidate(request) {
  const cache = await caches.open(CACHE_NAME);
  const cached = await cache.match(request);

  const fetched = fetch(request)
    .then((response) => {
      if (response.ok) cache.put(request, response.clone());
      return response;
    })
    .catch(() => cached || new Response("Offline", { status: 503 }));

  return cached || fetched;
}
<!-- offline.html -->
<!doctype html>
<html lang="zh">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>当前离线</title>
  </head>
  <body>
    <main>
      <h1>当前离线</h1>
      <p>网络恢复后,请重新加载这个页面。</p>
      <button type="button" onclick="location.reload()">重试</button>
    </main>
  </body>
</html>

这个例子对页面导航使用 Network First:先请求服务器,失败时才回到缓存或 offline.html。对 CSS、JS、字体和图片使用 Stale While Revalidate:先返回已有缓存,同时在后台刷新。新闻、价格、库存、登录后页面不要随便套这个策略。

更新和缓存失效

Service Worker 最容易出问题的地方是更新。当 sw.js 内容变化时,浏览器会安装新的 worker;如果旧页面还开着,新 worker 往往停在 waiting 状态。上面的注册代码会检测这个状态,询问用户是否刷新,并在用户同意后发送 SKIP_WAITING

worker 收到消息后调用 self.skipWaiting(),进入 activate,删除旧缓存,再用 clients.claim() 接管页面。如果缺少这段流程,用户可能在你部署修复后仍然持有旧的 app-cache-v1

缓存名要包含日期、发布号或 commit ID。构建产物带 hash 时,预缓存列表也要和构建结果同步。手写清单变得难维护时可以考虑 Workbox,但 Workbox 只能减少样板代码,不能替你判断业务数据是否适合缓存。

离线 UX 也是产品设计

离线支持不只是“能从缓存返回响应”。用户需要知道内容是否已保存、是否等待同步、同步失败后如何处理。表单提交不要放入 Cache API;页面侧应该把草稿或待发送任务写入 IndexedDB,然后在网络恢复时重试。Background Sync 可以帮忙,但浏览器支持并不一致,关键流程仍要准备 online 事件和可见的重试按钮。

给 Claude Code 的任务里,除了 offline.html,还要写清文案、重试按钮、草稿状态、同步失败提示。外勤应用至少要区分“已发送”“已保存在本机”“同步失败”三个状态。

常见失败

第一是 scope 不匹配。放在 /app/sw.js 的 worker 默认只控制 /app/,不是全站。如果要控制全站,把文件放在 /sw.js,注册时使用 scope /

第二是 cache.addAll() 里有 404。列表里只要一个文件不存在,整个 install 就会失败。让 Claude Code 添加文件后,一定在 DevTools Application 面板查看注册状态和 Cache Storage。

第三是私有数据。不要缓存 /api/me、账单页、管理后台 HTML、用户专属 JSON,除非你有非常明确的删除机制。浏览器缓存仍然是用户设备上的存储,共用电脑和账号切换场景都要考虑。

第四是没有更新提示。旧 worker 可能继续持有旧 JavaScript 和 CSS。给缓存名加版本,在 activate 删除旧缓存,并在新 worker waiting 时让用户选择刷新。

最后,Service Worker 不是永久存储。浏览器可能在空间不足时清理缓存;跨源 opaque 响应不容易估算大小;Service Worker 不能操作 DOM;它只在 HTTPS 或 localhost 下运行。没有测试真实流程前,不要承诺“完全离线”。

总结与咨询

Service Worker 能改善二次访问、离线体验和 PWA 质量。安全做法是在让 Claude Code 修改代码之前,先确定缓存归属、更新生命周期、隐私数据规则和离线页面。

ClaudeCodeLab 可以协助 PWA 改造、Service Worker 缓存设计、离线表单、Workbox 迁移,以及 Claude Code 实现评审。如果你希望站点更快,同时避免旧数据和隐私事故,可以从 Claude Code 培训与咨询 开始。

按本文最小示例在本地 Chrome 测试时,首次加载后 Application 面板会看到 claude-sw-demo-2026-06-02-v1。把 Network 切到 Offline 后刷新,会出现 offline.html。修改 CACHE_VERSION 后旧缓存会在 activate 阶段删除,适合作为上线前验证的起点。

#Claude Code #Service Worker #PWA #离线 #缓存
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。