用 Claude Code 实现 PWA:从 Manifest 到离线缓存
用 Claude Code 实现 PWA 的完整流程:manifest、图标、Service Worker、离线页面、缓存策略与验证。
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
| 检查项 | 位置 | 合格标准 |
|---|---|---|
| Manifest | Application > Manifest | 名称、start_url、图标无错误 |
| Service Worker | Application > Service Workers | /sw.js 已 activated |
| 离线导航 | Network Offline 后刷新 | 显示离线页或最近页面 |
| Cache Storage | Application > Cache Storage | static 和 runtime 缓存符合预期 |
| Lighthouse | Lighthouse 报告 | 性能、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,再逐步增加缓存范围,比一开始缓存全站更安全。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。