pwa-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

PWA 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 gone
Installing -> Waiting -> Active
     │           │           │
  install    activated    fetch events
 (precache)  when old SW  (runtime cache)
              is gone

Workbox: 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 responses
javascript
// 绝对禁止:无期限缓存所有内容(会导致存储膨胀)
// 绝对禁止:跳过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

关键决策

DecisionRecommendation
SW generatorgenerateSW for simple, injectManifest for custom
API cachingNetworkFirst for critical data
Static assetsCacheFirst with versioned filenames
Update strategyPrompt user for major changes
决策项推荐方案
Service Worker生成工具generateSW 适用于简单场景,injectManifest 适用于自定义场景
API缓存策略NetworkFirst 适用于关键数据
静态资产缓存CacheFirst 搭配带版本号的文件名
更新策略重大变更时提示用户

Related Skills

相关技能

  • caching-strategies
    - Backend caching patterns
  • core-web-vitals
    - Performance metrics
  • streaming-api-patterns
    - Real-time updates
  • caching-strategies
    - 后端缓存模式
  • core-web-vitals
    - 性能指标
  • streaming-api-patterns
    - 实时更新