pwa-patterns

📁 yonatangross/orchestkit 📅 Jan 22, 2026
26
总安装量
26
周安装量
#7607
全站排名
安装命令
npx skills add https://github.com/yonatangross/orchestkit --skill pwa-patterns

Agent 安装分布

antigravity 17
gemini-cli 15
claude-code 14
opencode 14
github-copilot 9
cursor 9

Skill 文档

PWA Patterns

Progressive Web App patterns using Workbox 7.x for service worker management, offline-first strategies, and app-like experiences.

Service Worker Lifecycle

Installing -> Waiting -> Active
     │           │           │
  install    activated    fetch events
 (precache)  when old SW  (runtime cache)
              is gone

Workbox: Generate Service Worker

// 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

// 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());

VitePWA Integration

// 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

{
  "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

Install Prompt Hook

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

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

// 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)

// 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

PWA Checklist

  • Service worker registered
  • Manifest with icons (192px + 512px maskable)
  • HTTPS enabled
  • Offline page works
  • Responsive design
  • Fast First Contentful Paint (< 1.8s)

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

Related Skills

  • caching-strategies – Backend caching patterns
  • core-web-vitals – Performance metrics
  • streaming-api-patterns – Real-time updates