Use Cases (更新: 2026/6/2)

用 Claude Code 实现 PWA:从 Manifest 到离线缓存

用 Claude Code 实现 PWA 的完整流程:manifest、图标、Service Worker、离线页面、缓存策略与验证。

用 Claude Code 实现 PWA:从 Manifest 到离线缓存

PWA(Progressive Web App,渐进式 Web 应用)不是把网页包装成 App 的魔法,而是一组可验证的 Web 能力:浏览器可以读取应用名称和图标,用户可以把站点安装到桌面或主屏幕,网络不好时也能看到有意义的离线页面,重复访问时常用资源可以更快加载。

真正容易出错的地方在于,PWA 不是只写一个 manifest.webmanifest。你还要准备图标、注册 Service Worker、设计缓存策略、提供离线 fallback,并用 Chrome DevTools 和 Lighthouse 做验证。一个图标路径 404、一个旧 HTML 被长期缓存,都会让用户看到奇怪的问题。

本文以 Claude Code 为开发助手,给出一套初学者也能复制运行的 PWA 实现流程。还不了解 Claude Code 的读者,可以先看Claude Code 入门指南。PWA 官方资料建议同时参考 web.dev PWA 学习指南、MDN 可安装 PWA 指南、MDN PWA 最佳实践、Chrome PWA 安装条件更新说明 和 Claude Code 官方文档。

先理解 PWA 的结构

PWA 的基本结构可以这样理解:页面通过 HTML 引入 manifest,应用入口注册 Service Worker,Service Worker 控制同源 GET 请求,并按资源类型决定走网络还是缓存。当导航请求失败时,它返回 offline.html,而不是让浏览器显示默认错误页。

用户打开站点
  -> index.html 引入 manifest.webmanifest
  -> register-sw.js 注册 /sw.js
  -> sw.js 预缓存 app shell
  -> fetch 事件按策略处理请求
  -> 离线导航返回 offline.html

开始前先决定三件事。

决策示例如果忽略会怎样
启动 URL 和 scope/ 或 /app/页面可能不受 Service Worker 控制
缓存对象HTML、CSS、JS、图片、离线页容易缓存 404 或旧内容
离线行为离线页、最近页面、API 错误用户看到空白页或误解数据状态

内容站、课程站和小型 SaaS 后台可以从保守策略开始:HTML 导航用 Network First,图片和图标用 Cache First,CSS、JS、字体用 Stale While Revalidate。需要更深入的缓存设计时,可以配合阅读Claude Code 缓存策略文章。

给 Claude Code 的提示词

不要只对 Claude Code 说“帮我做 PWA”。更好的方式是列出文件、策略和验证项,让它先确认项目结构,再修改代码。

请把这个现有的 Vite/React 应用改造成 PWA。

要求:
- 在 public 下新增 manifest.webmanifest
- 引用 192x192、512x512、maskable 512x512 PNG 图标
- 新增 public/offline.html
- 新增 public/sw.js 作为 Service Worker
- 从 src/register-sw.js 注册 Service Worker
- HTML navigation 使用 Network First
- 图片使用 Cache First
- CSS、JS、font 使用 Stale While Revalidate
- 不缓存 POST 请求和跨域请求
- 增加新版本可用时的刷新提示
- 最后输出 DevTools 和 Lighthouse 的手动检查清单

限制:
- 不要用空 fetch handler 伪装可安装
- 说明每个改动文件
- 标出依赖部署 base path 的路径

这样写的好处是,Claude Code 不只会生成代码,还会被迫说明假设。Masa 在一个小型课程落地页上尝试时,第一次出错并不是 Service Worker 语法,而是 start_url 和 scope 不匹配。把这些条件写进提示词,可以减少来回修补。

编写 Manifest 和图标

创建 public/manifest.webmanifest。name 是正式应用名称,short_name 用在主屏幕等空间较小的位置,start_url 是启动时打开的页面,scope 决定哪些 URL 属于这个应用。

{
  "id": "/",
  "name": "ClaudeCodeLab PWA Demo",
  "short_name": "CCLab",
  "description": "用 Claude Code 构建的离线可用 PWA 示例",
  "start_url": "/?source=pwa",
  "scope": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#0f766e",
  "orientation": "portrait-primary",
  "prefer_related_applications": false,
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-maskable-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ]
}

然后在 HTML head 中引用它。

<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#0f766e" />
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />

图标不要只写占位路径。至少准备 192x192 和 512x512 PNG,再准备一个带安全边距的 maskable 512x512 图标。Claude Code 可以帮你补引用,但最终仍要在浏览器里直接打开 /icons/icon-512.png,确认不是 404。

实现 Service Worker

下面的 public/sw.js 是一个适合起步的版本。它会预缓存应用外壳,激活时删除旧缓存,并且只处理同源 GET 请求。

const VERSION = "2026-06-02";
const STATIC_CACHE = `static-${VERSION}`;
const RUNTIME_CACHE = `runtime-${VERSION}`;

const APP_SHELL = [
  "/",
  "/offline.html",
  "/manifest.webmanifest",
  "/icons/icon-192.png",
  "/icons/icon-512.png",
  "/icons/icon-maskable-512.png"
];

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

self.addEventListener("activate", (event) => {
  const allowedCaches = [STATIC_CACHE, RUNTIME_CACHE];

  event.waitUntil(
    caches
      .keys()
      .then((keys) =>
        Promise.all(
          keys
            .filter((key) => !allowedCaches.includes(key))
            .map((key) => caches.delete(key))
        )
      )
      .then(() => self.clients.claim())
  );
});

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(networkFirstPage(request));
    return;
  }

  if (request.destination === "image") {
    event.respondWith(cacheFirst(request));
    return;
  }

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

async function networkFirstPage(request) {
  const cache = await caches.open(RUNTIME_CACHE);

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

async function cacheFirst(request) {
  const cached = await caches.match(request);
  if (cached) return cached;

  const response = await fetch(request);
  if (response.ok) {
    const cache = await caches.open(RUNTIME_CACHE);
    await cache.put(request, response.clone());
  }
  return response;
}

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

  const networkPromise = fetch(request)
    .then((response) => {
      if (response.ok) cache.put(request, response.clone());
      return response;
    })
    .catch(() => undefined);

  if (cached) return cached;
  return (await networkPromise) || new Response("Network error", { status: 504 });
}

这个版本故意不缓存 POST、跨域资源和用户私有 API。登录状态、支付、库存、权限、购物车都不应该被随意放进通用运行时缓存。缓存是一种存储,不能因为它能提升速度就忽略数据安全。

离线页面和注册代码

public/offline.html 应该简单、可靠,不依赖远程字体、广告脚本或分析脚本。

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>当前离线</title>
  </head>
  <body>
    <main>
      <h1>当前离线</h1>
      <p>网络恢复后请刷新页面。最近打开过的页面可能仍然可用。</p>
      <p><a href="/">返回首页</a></p>
    </main>
  </body>
</html>

再创建 src/register-sw.js。

export async function registerServiceWorker() {
  if (!("serviceWorker" in navigator)) return;

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

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

        worker.addEventListener("statechange", () => {
          if (worker.state === "installed" && navigator.serviceWorker.controller) {
            document.querySelector("[data-refresh-app]")?.removeAttribute("hidden");
          }
        });
      });
    } catch (error) {
      console.error("Service Worker registration failed:", error);
    }
  });
}

入口文件中只调用一次。

import { registerServiceWorker } from "./register-sw.js";

registerServiceWorker();

更新提示很重要,因为 Service Worker 有自己的生命周期。新版本下载后可能处于 waiting 状态,如果不提示用户刷新,就容易出现旧 HTML 配新 JS 的问题。

安装按钮和验证

Chromium 浏览器在满足条件时可能触发 beforeinstallprompt。它不是所有浏览器都有的通用标准信号,所以把安装按钮当作渐进增强即可。

let deferredPrompt = null;

window.addEventListener("beforeinstallprompt", (event) => {
  event.preventDefault();
  deferredPrompt = event;
  document.querySelector("[data-install-app]")?.removeAttribute("hidden");
});

document.querySelector("[data-install-app]")?.addEventListener("click", async () => {
  if (!deferredPrompt) return;
  deferredPrompt.prompt();
  const choice = await deferredPrompt.userChoice;
  console.info("Install result:", choice.outcome);
  deferredPrompt = null;
});

验证时,不要只追求旧版 Lighthouse 的 PWA 分数。现在更可靠的流程是:用 DevTools Application 面板检查 Manifest、Service Worker、Cache Storage 和离线行为,再用 Lighthouse 检查性能、可访问性、最佳实践和 SEO。

npm run build
npx serve dist -l 4173
npx lighthouse http://localhost:4173 --view --only-categories=performance,accessibility,best-practices,seo
检查项位置合格标准
ManifestApplication > Manifest名称、start_url、图标无错误
Service WorkerApplication > Service Workers/sw.js 已 activated
离线导航Network Offline 后刷新显示离线页或最近页面
Cache StorageApplication > Cache Storagestatic 和 runtime 缓存符合预期
LighthouseLighthouse 报告性能、SEO、无障碍没有明显回退

使用场景、CTA 和常见陷阱

PWA 最适合高频访问、网络不稳定、用户有理由放到主屏幕的场景。

场景PWA 价值变现或业务导线
技术博客和课程库通勤时也能继续阅读付费模板、课程、咨询
内部仪表盘每天更快打开工作区团队培训和导入支持
活动或场馆指南会场网络拥堵时仍能看日程赞助位、资料下载、留资
电商和预约页面图片和最近浏览更快会员注册、购物车恢复

ClaudeCodeLab 的 CTA 不应该只是“我们能做 PWA”,而应该是“我们能让重复访问、安装点击、离线 fallback 和购买点击被测量”。需要 Claude Code 提示词和实现模板时,可以查看产品资料库。如果是团队项目,建议把 PWA 与缓存审查、部署路径验证、分析事件一起交付。

常见陷阱有五个:scope 和 start_url 不一致;HTML 使用 Cache First 导致发布后不更新;把用户私有 API 响应缓存下来;图标路径 404;调试时忘记注销旧 Service Worker。每次排查都应清空 Cache Storage,Unregister Service Worker,然后重新走一次首次访问流程。

实测结果

Masa 在一个小型 Vite 课程落地页中测试时,manifest、注册代码和离线页都能很快由 Claude Code 生成。真正耗时的是验证:逐个打开图标 URL、切到 Offline 后刷新、发布新版本后清理旧缓存。结论很明确:先交付最小可用的离线 fallback,再逐步增加缓存范围,比一开始缓存全站更安全。

#Claude Code #PWA #Service Worker #offline #mobile
免费

免费 PDF: Claude Code 速查表

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

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

让 Claude Code 真正进入可验证的工作流

先用免费 PDF 固定基础,再用 Gumroad 教材复用工作流;如果涉及团队导入、权限或收入路径,可以直接咨询。

Masa

关于作者

Masa

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