nextjs-pwa
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNext.js PWA Skill
Next.js PWA 开发指南
Quick Reference
快速参考
| Task | Approach | Reference |
|---|---|---|
| Add PWA to Next.js app | Serwist (recommended) | This file → Quick Start |
| Add PWA without dependencies | Manual SW | references/service-worker-manual.md |
| Configure caching | Serwist defaultCache or custom | references/caching-strategies.md |
| Add offline support | App shell + IndexedDB | references/offline-data.md |
| Push notifications | VAPID + web-push | references/push-notifications.md |
| Fix iOS issues | Safari/WebKit workarounds | references/ios-quirks.md |
| Debug SW / Lighthouse | DevTools + common fixes | references/troubleshooting.md |
| Migrate from next-pwa | Serwist migration | references/serwist-setup.md |
| 任务 | 实现方式 | 参考文档 |
|---|---|---|
| 为Next.js应用添加PWA功能 | Serwist(推荐) | 本文档 → 快速开始 |
| 无依赖添加PWA功能 | 手动实现Service Worker | references/service-worker-manual.md |
| 配置缓存策略 | Serwist默认缓存或自定义配置 | references/caching-strategies.md |
| 添加离线支持 | 应用壳架构 + IndexedDB | references/offline-data.md |
| 实现推送通知 | VAPID + web-push | references/push-notifications.md |
| 修复iOS兼容问题 | Safari/WebKit兼容方案 | references/ios-quirks.md |
| 调试Service Worker / Lighthouse检测 | DevTools + 常见问题修复 | references/troubleshooting.md |
| 从next-pwa迁移 | Serwist迁移指南 | references/serwist-setup.md |
Quick Start — Serwist (Recommended)
快速开始 — Serwist(推荐方案)
Serwist is the actively maintained successor to next-pwa, built for App Router.
Serwist是next-pwa的官方维护继任者,专为App Router打造。
1. Install
1. 安装依赖
bash
npm install @serwist/next && npm install -D serwistbash
npm install @serwist/next && npm install -D serwist2. Create app/manifest.ts
app/manifest.ts2. 创建 app/manifest.ts
app/manifest.tsts
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "My App",
short_name: "App",
description: "My Progressive Web App",
start_url: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#000000",
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" },
],
};
}ts
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "My App",
short_name: "App",
description: "My Progressive Web App",
start_url: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#000000",
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" },
],
};
}3. Create app/sw.ts
(service worker)
app/sw.ts3. 创建 app/sw.ts
(Service Worker文件)
app/sw.tsts
import { defaultCache } from "@serwist/next/worker";
import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
import { Serwist } from "serwist";
declare global {
interface WorkerGlobalScope extends SerwistGlobalConfig {
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
}
}
declare const self: ServiceWorkerGlobalScope;
const serwist = new Serwist({
precacheEntries: self.__SW_MANIFEST,
skipWaiting: true,
clientsClaim: true,
navigationPreload: true,
runtimeCaching: defaultCache,
});
serwist.addEventListeners();ts
import { defaultCache } from "@serwist/next/worker";
import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
import { Serwist } from "serwist";
declare global {
interface WorkerGlobalScope extends SerwistGlobalConfig {
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
}
}
declare const self: ServiceWorkerGlobalScope;
const serwist = new Serwist({
precacheEntries: self.__SW_MANIFEST,
skipWaiting: true,
clientsClaim: true,
navigationPreload: true,
runtimeCaching: defaultCache,
});
serwist.addEventListeners();4. Update next.config.ts
next.config.ts4. 更新 next.config.ts
next.config.tsts
import withSerwist from "@serwist/next";
const nextConfig = {
// your existing config
};
export default withSerwist({
swSrc: "app/sw.ts",
swDest: "public/sw.js",
disable: process.env.NODE_ENV === "development",
})(nextConfig);That's it — 4 files for a working PWA. Run and test with Lighthouse.
next buildts
import withSerwist from "@serwist/next";
const nextConfig = {
// 现有配置
};
export default withSerwist({
swSrc: "app/sw.ts",
swDest: "public/sw.js",
disable: process.env.NODE_ENV === "development",
})(nextConfig);完成以上4步即可搭建可运行的PWA。执行后,使用Lighthouse进行测试。
next buildQuick Start — Manual (No Dependencies)
快速开始 — 手动实现(无依赖)
Use this when you want zero dependencies or are using .
output: "export"当你需要零依赖或使用静态导出时,可采用此方案。
output: "export"1. Create app/manifest.ts
app/manifest.ts1. 创建 app/manifest.ts
app/manifest.tsSame as above.
与上述步骤相同。
2. Create public/sw.js
public/sw.js2. 创建 public/sw.js
public/sw.jsjs
const CACHE_NAME = "app-v1";
const PRECACHE_URLS = ["/", "/offline"];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
);
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
)
);
self.clients.claim();
});
self.addEventListener("fetch", (event) => {
if (event.request.mode === "navigate") {
event.respondWith(
fetch(event.request).catch(() => caches.match("/offline"))
);
return;
}
event.respondWith(
caches.match(event.request).then((cached) => cached || fetch(event.request))
);
});js
const CACHE_NAME = "app-v1";
const PRECACHE_URLS = ["/", "/offline"];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
);
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
)
);
self.clients.claim();
});
self.addEventListener("fetch", (event) => {
if (event.request.mode === "navigate") {
event.respondWith(
fetch(event.request).catch(() => caches.match("/offline"))
);
return;
}
event.respondWith(
caches.match(event.request).then((cached) => cached || fetch(event.request))
);
});3. Register SW in layout
3. 在布局中注册Service Worker
tsx
// app/components/ServiceWorkerRegistration.tsx
"use client";
import { useEffect } from "react";
export function ServiceWorkerRegistration() {
useEffect(() => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js");
}
}, []);
return null;
}Add to your root layout.
<ServiceWorkerRegistration />tsx
// app/components/ServiceWorkerRegistration.tsx
"use client";
import { useEffect } from "react";
export function ServiceWorkerRegistration() {
useEffect(() => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js");
}
}, []);
return null;
}将添加到根布局组件中。
<ServiceWorkerRegistration />Decision Framework
方案选择框架
| Scenario | Recommendation |
|---|---|
| App Router, wants caching out of the box | Serwist |
Static export ( | Manual SW |
| Migrating from next-pwa | Serwist (drop-in successor) |
| Need push notifications | Either — see references/push-notifications.md |
| Need granular cache control | Serwist with custom routes |
| Zero dependencies required | Manual SW |
| Minimal PWA (just installable) | Manual SW |
| 场景 | 推荐方案 |
|---|---|
| 使用App Router,需要开箱即用的缓存功能 | Serwist |
静态导出( | 手动实现Service Worker |
| 从next-pwa迁移 | Serwist(无缝替代方案) |
| 需要推送通知功能 | 两种方案均可 → 参考references/push-notifications.md |
| 需要精细的缓存控制 | 带自定义路由的Serwist方案 |
| 要求零依赖 | 手动实现Service Worker |
| 轻量PWA(仅需可安装) | 手动实现Service Worker |
Web App Manifest
Web应用清单
Next.js 13.3+ supports natively. This generates at build time.
app/manifest.ts/manifest.webmanifestNext.js 13.3+原生支持,构建时会自动生成文件。
app/manifest.ts/manifest.webmanifestKey fields
核心字段
ts
{
name: "Full App Name", // install dialog, splash screen
short_name: "App", // home screen label (≤12 chars)
description: "What the app does",
start_url: "/", // entry point on launch
display: "standalone", // standalone | fullscreen | minimal-ui | browser
orientation: "portrait", // optional: lock orientation
background_color: "#ffffff", // splash screen background
theme_color: "#000000", // browser chrome color
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" },
{ src: "/icon-maskable.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
],
screenshots: [ // optional: richer install UI
{ src: "/screenshot-wide.png", sizes: "1280x720", type: "image/png", form_factor: "wide" },
{ src: "/screenshot-narrow.png", sizes: "640x1136", type: "image/png", form_factor: "narrow" },
],
}ts
{
name: "Full App Name", // 安装弹窗、启动屏显示名称
short_name: "App", // 主屏幕标签(≤12字符)
description: "What the app does",
start_url: "/", // 启动入口
display: "standalone", // 显示模式:standalone | fullscreen | minimal-ui | browser
orientation: "portrait", // 可选:锁定屏幕方向
background_color: "#ffffff", // 启动屏背景色
theme_color: "#000000", // 浏览器导航栏颜色
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" },
{ src: "/icon-maskable.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
],
screenshots: [ // 可选:丰富安装界面
{ src: "/screenshot-wide.png", sizes: "1280x720", type: "image/png", form_factor: "wide" },
{ src: "/screenshot-narrow.png", sizes: "640x1136", type: "image/png", form_factor: "narrow" },
],
}Manifest tips
清单配置技巧
- Always include both 192x192 and 512x512 icons (Lighthouse requirement)
- Add a icon for Android adaptive icons
maskable - enable the richer install sheet on Android/desktop Chrome
screenshots - should match your
theme_colorin layout<meta name="theme-color">
- 必须同时包含192x192和512x512尺寸的图标(Lighthouse检测要求)
- 添加类型图标以支持Android自适应图标
maskable - 字段可在Android/桌面版Chrome中启用更丰富的安装界面
screenshots - 应与布局中的
theme_color保持一致<meta name="theme-color">
Service Worker Essentials
Service Worker 核心知识
Lifecycle
生命周期
- Install — SW downloaded, event fires, precache assets
install - Waiting — New SW waits for all tabs to close (unless )
skipWaiting - Activate — Old caches cleaned up, SW takes control
- Fetch — SW intercepts network requests
- 安装 — 下载Service Worker,触发事件,预缓存资源
install - 等待 — 新的Service Worker等待所有标签页关闭(除非设置)
skipWaiting - 激活 — 清理旧缓存,Service Worker接管控制权
- 请求拦截 — Service Worker拦截网络请求
Update flow
更新流程
When a new SW is detected:
- — immediately activates (may break in-flight requests)
skipWaiting: true - Without — waits for all tabs to close, then activates
skipWaiting - Notify users of updates with or manual
workbox-windowlistenercontrollerchange
检测到新的Service Worker时:
- — 立即激活(可能中断正在进行的请求)
skipWaiting: true - 未设置— 等待所有标签页关闭后激活
skipWaiting - 可通过或手动监听
workbox-window事件通知用户更新controllerchange
Registration scope
注册作用域
- SW at controls all pages under
/sw.js/ - SW at only controls
/app/sw.js/app/* - Always place SW at root unless you have a specific reason not to
- 位于的Service Worker控制
/sw.js下的所有页面/ - 位于的Service Worker仅控制
/app/sw.js路径下的页面/app/* - 除非有特殊需求,否则应将Service Worker放在根目录
Caching Strategies Quick Reference
缓存策略快速参考
| Strategy | Use For | Serwist Class |
|---|---|---|
| Cache First | Static assets, fonts, images | |
| Network First | API data, HTML pages | |
| Stale While Revalidate | Semi-static content (CSS/JS) | |
| Network Only | Auth endpoints, real-time data | |
| Cache Only | Precached content only | |
Serwist's provides sensible defaults. For custom strategies, see references/caching-strategies.md.
defaultCache| 策略 | 适用场景 | Serwist对应类 |
|---|---|---|
| 缓存优先 | 静态资源、字体、图片 | |
| 网络优先 | API数据、HTML页面 | |
| 缓存回源更新 | 半静态内容(CSS/JS) | |
| 仅网络 | 授权接口、实时数据 | |
| 仅缓存 | 仅预缓存内容 | |
Serwist的提供了合理的默认配置。如需自定义策略,请参考references/caching-strategies.md。
defaultCacheOffline Support Basics
离线支持基础
App shell pattern
应用壳架构
Precache the app shell (layout, styles, scripts) so the UI loads instantly offline. Dynamic content loads from cache or shows a fallback.
预缓存应用壳(布局、样式、脚本),确保UI可立即离线加载。动态内容从缓存读取或显示降级页面。
Online/offline detection hook
在线/离线状态检测钩子
tsx
"use client";
import { useSyncExternalStore } from "react";
function subscribe(callback: () => void) {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);
return () => {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
}
export function useOnlineStatus() {
return useSyncExternalStore(
subscribe,
() => navigator.onLine,
() => true // SSR: assume online
);
}tsx
"use client";
import { useSyncExternalStore } from "react";
function subscribe(callback: () => void) {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);
return () => {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
}
export function useOnlineStatus() {
return useSyncExternalStore(
subscribe,
() => navigator.onLine,
() => true // SSR环境默认假设在线
);
}Offline fallback page
离线降级页面
Create and precache in your SW. When navigation fails, serve this page.
app/offline/page.tsx/offlineFor IndexedDB, background sync, and advanced offline patterns, see references/offline-data.md.
创建,并在Service Worker中预缓存路径。当导航失败时,返回该页面。
app/offline/page.tsx/offline如需IndexedDB、后台同步等高级离线方案,请参考references/offline-data.md。
Install Prompt Handling
安装提示处理
beforeinstallprompt
(Chrome/Edge/Android)
beforeinstallpromptbeforeinstallprompt
事件(Chrome/Edge/Android)
beforeinstallprompttsx
"use client";
import { useState, useEffect } from "react";
export function InstallPrompt() {
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
useEffect(() => {
const handler = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e);
};
window.addEventListener("beforeinstallprompt", handler);
return () => window.removeEventListener("beforeinstallprompt", handler);
}, []);
if (!deferredPrompt) return null;
return (
<button
onClick={async () => {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === "accepted") setDeferredPrompt(null);
}}
>
Install App
</button>
);
}tsx
"use client";
import { useState, useEffect } from "react";
export function InstallPrompt() {
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
useEffect(() => {
const handler = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e);
};
window.addEventListener("beforeinstallprompt", handler);
return () => window.removeEventListener("beforeinstallprompt", handler);
}, []);
if (!deferredPrompt) return null;
return (
<button
onClick={async () => {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === "accepted") setDeferredPrompt(null);
}}
>
安装应用
</button>
);
}iOS detection
iOS设备检测
iOS doesn't fire . Detect iOS and show manual instructions:
beforeinstallpromptts
function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream;
}
function isStandalone() {
return window.matchMedia("(display-mode: standalone)").matches
|| (navigator as any).standalone === true;
}Show a banner: "Tap Share then Add to Home Screen" for iOS Safari users.
iOS不会触发事件。需检测iOS设备并显示手动安装指引:
beforeinstallpromptts
function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream;
}
function isStandalone() {
return window.matchMedia("(display-mode: standalone)").matches
|| (navigator as any).standalone === true;
}显示提示横幅:"点击分享按钮,选择添加到主屏幕"(针对iOS Safari用户)。
Push Notifications Quick Start
推送通知快速开始
1. Generate VAPID keys
1. 生成VAPID密钥
bash
npx web-push generate-vapid-keysbash
npx web-push generate-vapid-keys2. Subscribe in client
2. 客户端订阅
ts
async function subscribeToPush() {
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
});
await fetch("/api/push/subscribe", {
method: "POST",
body: JSON.stringify(sub),
});
}ts
async function subscribeToPush() {
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
});
await fetch("/api/push/subscribe", {
method: "POST",
body: JSON.stringify(sub),
});
}3. Handle in SW
3. Service Worker中处理
ts
self.addEventListener("push", (event) => {
const data = event.data?.json() ?? { title: "Notification" };
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: "/icon-192.png",
})
);
});For server-side sending, VAPID setup, and full implementation, see references/push-notifications.md.
ts
self.addEventListener("push", (event) => {
const data = event.data?.json() ?? { title: "Notification" };
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: "/icon-192.png",
})
);
});如需服务端发送、VAPID配置及完整实现,请参考references/push-notifications.md。
Troubleshooting Cheat Sheet
问题排查速查表
| Problem | Fix |
|---|---|
| SW not updating | Add |
| App not installable | Check manifest: needs |
| Stale content after deploy | Bump cache version or use content-hashed URLs |
| SW registered in dev | Disable in dev: |
| iOS not showing install | iOS has no install prompt — show manual instructions |
| Lighthouse PWA fails | Check HTTPS, valid manifest, registered SW, offline page |
| Next.js rewrite conflicts | Ensure SW is served from |
For detailed debugging steps, see references/troubleshooting.md.
| 问题 | 解决方法 |
|---|---|
| Service Worker未更新 | 添加 |
| 应用无法安装 | 检查清单:需包含 |
| 部署后内容未更新 | 升级缓存版本或使用带内容哈希的URL |
| 开发环境中注册了Service Worker | 在开发环境禁用: |
| iOS未显示安装提示 | iOS无自动安装提示 — 显示手动指引 |
| Lighthouse PWA检测失败 | 检查HTTPS、有效清单、已注册的Service Worker、离线页面 |
| Next.js路由重写冲突 | 确保Service Worker从 |
如需详细调试步骤,请参考references/troubleshooting.md。
Assets & Templates
资源与模板
- — Complete app/manifest.ts with all fields
assets/manifest-template.ts - — Serwist SW with custom routes and offline fallback
assets/sw-serwist-template.ts - — Manual SW with all strategies
assets/sw-manual-template.js - — next.config.ts with withSerwist
assets/next-config-serwist.ts
- — 完整的app/manifest.ts模板(包含所有字段)
assets/manifest-template.ts - — 带自定义路由和离线降级的Serwist Service Worker模板
assets/sw-serwist-template.ts - — 包含所有策略的手动实现Service Worker模板
assets/sw-manual-template.js - — 集成withSerwist的next.config.ts模板
assets/next-config-serwist.ts
Generator Script
生成脚本
bash
python scripts/generate_pwa_config.py <project-name> --approach serwist|manual [--push] [--offline]Scaffolds PWA files based on chosen approach and features.
bash
python scripts/generate_pwa_config.py <project-name> --approach serwist|manual [--push] [--offline]根据选择的实现方式和功能,自动生成PWA相关文件。