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_urlscope 不匹配。把这些条件写进提示词,可以减少来回修补。

编写 Manifest 和图标

创建 public/manifest.webmanifestname 是正式应用名称,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 与缓存审查、部署路径验证、分析事件一起交付。

常见陷阱有五个:scopestart_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,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

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