pwa-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePWA Patterns
PWA 设计模式
Progressive Web App patterns using Workbox 7.x for service worker management, offline-first strategies, and app-like experiences.
使用Workbox 7.x进行Service Worker管理、离线优先策略实现类应用体验的渐进式Web应用(PWA)设计模式。
Service Worker Lifecycle
Service Worker 生命周期
Installing -> Waiting -> Active
│ │ │
install activated fetch events
(precache) when old SW (runtime cache)
is goneInstalling -> Waiting -> Active
│ │ │
install activated fetch events
(precache) when old SW (runtime cache)
is goneWorkbox: Generate Service Worker
Workbox:生成Service Worker
javascript
// build-sw.js (Node.js)
const { generateSW } = require('workbox-build');
async function buildServiceWorker() {
await generateSW({
globDirectory: 'dist/',
globPatterns: ['**/*.{html,js,css,png,jpg,json,woff2}'],
swDest: 'dist/sw.js',
clientsClaim: true,
skipWaiting: true,
navigateFallback: '/index.html',
navigateFallbackDenylist: [/^\/api\//],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\//,
handler: 'NetworkFirst',
options: { cacheName: 'api-cache', networkTimeoutSeconds: 10 },
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
handler: 'CacheFirst',
options: { cacheName: 'images', expiration: { maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 } },
},
],
});
}javascript
// build-sw.js (Node.js)
const { generateSW } = require('workbox-build');
async function buildServiceWorker() {
await generateSW({
globDirectory: 'dist/',
globPatterns: ['**/*.{html,js,css,png,jpg,json,woff2}'],
swDest: 'dist/sw.js',
clientsClaim: true,
skipWaiting: true,
navigateFallback: '/index.html',
navigateFallbackDenylist: [/^\/api\//],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\//,
handler: 'NetworkFirst',
options: { cacheName: 'api-cache', networkTimeoutSeconds: 10 },
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
handler: 'CacheFirst',
options: { cacheName: 'images', expiration: { maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 } },
},
],
});
}Caching Strategies
缓存策略
javascript
// CacheFirst: Static assets that rarely change
registerRoute(/\.(?:js|css|woff2)$/, new CacheFirst({
cacheName: 'static-v1',
plugins: [new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 365 * 24 * 60 * 60 })],
}));
// NetworkFirst: API calls (fresh data preferred)
registerRoute(/\/api\//, new NetworkFirst({
cacheName: 'api-cache',
networkTimeoutSeconds: 10,
plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })],
}));
// StaleWhileRevalidate: User avatars, non-critical images
registerRoute(/\/avatars\//, new StaleWhileRevalidate({ cacheName: 'avatars' }));
// NetworkOnly: Auth endpoints
registerRoute(/\/auth\//, new NetworkOnly());javascript
// CacheFirst:很少变更的静态资产
registerRoute(/\.(?:js|css|woff2)$/, new CacheFirst({
cacheName: 'static-v1',
plugins: [new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 365 * 24 * 60 * 60 })],
}));
// NetworkFirst:API调用(优先获取最新数据)
registerRoute(/\/api\//, new NetworkFirst({
cacheName: 'api-cache',
networkTimeoutSeconds: 10,
plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })],
}));
// StaleWhileRevalidate:用户头像、非关键图片
registerRoute(/\/avatars\//, new StaleWhileRevalidate({ cacheName: 'avatars' }));
// NetworkOnly:认证端点
registerRoute(/\/auth\//, new NetworkOnly());VitePWA Integration
VitePWA 集成
typescript
// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: [{ urlPattern: /^https:\/\/api\./, handler: 'NetworkFirst' }],
},
manifest: {
name: 'My PWA App',
short_name: 'MyPWA',
theme_color: '#4f46e5',
icons: [
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
],
},
}),
],
});typescript
// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: [{ urlPattern: /^https:\/\/api\./, handler: 'NetworkFirst' }],
},
manifest: {
name: 'My PWA App',
short_name: 'MyPWA',
theme_color: '#4f46e5',
icons: [
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
],
},
}),
],
});Web App Manifest
Web应用清单
json
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4f46e5",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}json
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4f46e5",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}React Hooks
React Hooks
Install Prompt Hook
安装提示Hook
tsx
import { useState, useEffect } from 'react';
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}
export function useInstallPrompt() {
const [installPrompt, setInstallPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [isInstalled, setIsInstalled] = useState(false);
useEffect(() => {
const handler = (e: BeforeInstallPromptEvent) => { e.preventDefault(); setInstallPrompt(e); };
window.addEventListener('beforeinstallprompt', handler as EventListener);
if (window.matchMedia('(display-mode: standalone)').matches) setIsInstalled(true);
return () => window.removeEventListener('beforeinstallprompt', handler as EventListener);
}, []);
const promptInstall = async () => {
if (!installPrompt) return false;
await installPrompt.prompt();
const { outcome } = await installPrompt.userChoice;
setInstallPrompt(null);
if (outcome === 'accepted') { setIsInstalled(true); return true; }
return false;
};
return { canInstall: !!installPrompt, isInstalled, promptInstall };
}tsx
import { useState, useEffect } from 'react';
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}
export function useInstallPrompt() {
const [installPrompt, setInstallPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [isInstalled, setIsInstalled] = useState(false);
useEffect(() => {
const handler = (e: BeforeInstallPromptEvent) => { e.preventDefault(); setInstallPrompt(e); };
window.addEventListener('beforeinstallprompt', handler as EventListener);
if (window.matchMedia('(display-mode: standalone)').matches) setIsInstalled(true);
return () => window.removeEventListener('beforeinstallprompt', handler as EventListener);
}, []);
const promptInstall = async () => {
if (!installPrompt) return false;
await installPrompt.prompt();
const { outcome } = await installPrompt.userChoice;
setInstallPrompt(null);
if (outcome === 'accepted') { setIsInstalled(true); return true; }
return false;
};
return { canInstall: !!installPrompt, isInstalled, promptInstall };
}Offline Status Hook
离线状态Hook
tsx
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}tsx
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}Background Sync
后台同步
javascript
// sw.js
import { BackgroundSyncPlugin } from 'workbox-background-sync';
import { registerRoute } from 'workbox-routing';
import { NetworkOnly } from 'workbox-strategies';
registerRoute(
/\/api\/forms/,
new NetworkOnly({ plugins: [new BackgroundSyncPlugin('formQueue', { maxRetentionTime: 24 * 60 })] }),
'POST'
);javascript
// sw.js
import { BackgroundSyncPlugin } from 'workbox-background-sync';
import { registerRoute } from 'workbox-routing';
import { NetworkOnly } from 'workbox-strategies';
registerRoute(
/\/api\/forms/,
new NetworkOnly({ plugins: [new BackgroundSyncPlugin('formQueue', { maxRetentionTime: 24 * 60 })] }),
'POST'
);Anti-Patterns (FORBIDDEN)
反模式(禁止操作)
javascript
// NEVER: Cache everything with no expiration (storage bloat)
// NEVER: Skip clientsClaim (old tabs stay on old SW)
// NEVER: Cache authentication tokens (security risk)
// NEVER: Precache dynamic content (changes frequently)
// NEVER: Forget offline fallback for navigation
// NEVER: Cache POST responsesjavascript
// 绝对禁止:无期限缓存所有内容(会导致存储膨胀)
// 绝对禁止:跳过clientsClaim(旧标签页仍使用旧的SW)
// 绝对禁止:缓存认证令牌(存在安全风险)
// 绝对禁止:预缓存动态内容(频繁变更)
// 绝对禁止:忘记为导航设置离线回退页
// 绝对禁止:缓存POST响应PWA Checklist
PWA 检查清单
- Service worker registered
- Manifest with icons (192px + 512px maskable)
- HTTPS enabled
- Offline page works
- Responsive design
- Fast First Contentful Paint (< 1.8s)
- 已注册Service Worker
- 包含图标(192px + 512px 可遮罩图标)的应用清单
- 已启用HTTPS
- 离线页面可正常工作
- 响应式设计
- 首屏内容绘制速度快(<1.8秒)
Key Decisions
关键决策
| Decision | Recommendation |
|---|---|
| SW generator | generateSW for simple, injectManifest for custom |
| API caching | NetworkFirst for critical data |
| Static assets | CacheFirst with versioned filenames |
| Update strategy | Prompt user for major changes |
| 决策项 | 推荐方案 |
|---|---|
| Service Worker生成工具 | generateSW 适用于简单场景,injectManifest 适用于自定义场景 |
| API缓存策略 | NetworkFirst 适用于关键数据 |
| 静态资产缓存 | CacheFirst 搭配带版本号的文件名 |
| 更新策略 | 重大变更时提示用户 |
Related Skills
相关技能
- - Backend caching patterns
caching-strategies - - Performance metrics
core-web-vitals - - Real-time updates
streaming-api-patterns
- - 后端缓存模式
caching-strategies - - 性能指标
core-web-vitals - - 实时更新
streaming-api-patterns