pwa-development

安装量: 35
排名: #19546

安装

npx skills add https://github.com/alinaqi/claude-bootstrap --skill pwa-development

PWA Development Skill

Load with: base.md

Purpose: Build Progressive Web Apps that work offline, install like native apps, and deliver fast, reliable experiences across all devices.

Core PWA Requirements ┌─────────────────────────────────────────────────────────────────┐ │ THE THREE PILLARS OF PWA │ │ ───────────────────────────────────────────────────────────── │ │ │ │ 1. HTTPS │ │ Required for service workers and security. │ │ localhost allowed for development. │ │ │ │ 2. SERVICE WORKER │ │ JavaScript that runs in background. │ │ Enables offline, caching, push notifications. │ │ │ │ 3. WEB APP MANIFEST │ │ JSON file describing app metadata. │ │ Enables installation and app-like experience. │ ├─────────────────────────────────────────────────────────────────┤ │ INSTALLABILITY CRITERIA (Chrome) │ │ ───────────────────────────────────────────────────────────── │ │ • HTTPS (or localhost) │ │ • Service worker with fetch handler │ │ • Web app manifest with: name, icons (192px + 512px), │ │ start_url, display: standalone/fullscreen/minimal-ui │ └─────────────────────────────────────────────────────────────────┘

Web App Manifest Required Fields { "name": "My Progressive Web App", "short_name": "MyPWA", "description": "A description of what the app does", "start_url": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#000000", "icons": [ { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "/icons/icon-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] }

Enhanced Manifest (Full Features) { "name": "My Progressive Web App", "short_name": "MyPWA", "description": "A full-featured PWA", "start_url": "/?source=pwa", "scope": "/", "display": "standalone", "orientation": "portrait-primary", "background_color": "#ffffff", "theme_color": "#3367D6", "dir": "ltr", "lang": "en", "categories": ["productivity", "utilities"],

"icons": [ { "src": "/icons/icon-72.png", "sizes": "72x72", "type": "image/png" }, { "src": "/icons/icon-96.png", "sizes": "96x96", "type": "image/png" }, { "src": "/icons/icon-128.png", "sizes": "128x128", "type": "image/png" }, { "src": "/icons/icon-144.png", "sizes": "144x144", "type": "image/png" }, { "src": "/icons/icon-152.png", "sizes": "152x152", "type": "image/png" }, { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/icon-384.png", "sizes": "384x384", "type": "image/png" }, { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "/icons/icon-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ],

"screenshots": [ { "src": "/screenshots/desktop.png", "sizes": "1280x720", "type": "image/png", "form_factor": "wide" }, { "src": "/screenshots/mobile.png", "sizes": "750x1334", "type": "image/png", "form_factor": "narrow" } ],

"shortcuts": [ { "name": "New Item", "short_name": "New", "description": "Create a new item", "url": "/new?source=shortcut", "icons": [{ "src": "/icons/shortcut-new.png", "sizes": "192x192" }] } ],

"share_target": { "action": "/share", "method": "POST", "enctype": "multipart/form-data", "params": { "title": "title", "text": "text", "url": "url", "files": [{ "name": "files", "accept": ["image/*"] }] } },

"protocol_handlers": [ { "protocol": "web+myapp", "url": "/handle?url=%s" } ],

"file_handlers": [ { "action": "/open-file", "accept": { "text/plain": [".txt"] } } ] }

Manifest Checklist name and short_name defined start_url set (use query param for analytics) display set to standalone or fullscreen Icons: 192x192 and 512x512 minimum Maskable icon included for Android adaptive icons theme_color matches app design background_color for splash screen Screenshots for richer install UI (optional) Shortcuts for quick actions (optional) Service Worker Patterns Basic Service Worker // sw.js const CACHE_NAME = 'app-cache-v1'; const STATIC_ASSETS = [ '/', '/index.html', '/styles/main.css', '/scripts/app.js', '/offline.html' ];

// Install: Cache static assets self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME) .then((cache) => cache.addAll(STATIC_ASSETS)) .then(() => self.skipWaiting()) ); });

// Activate: Clean old caches self.addEventListener('activate', (event) => { event.waitUntil( caches.keys() .then((keys) => Promise.all( keys .filter((key) => key !== CACHE_NAME) .map((key) => caches.delete(key)) )) .then(() => self.clients.claim()) ); });

// Fetch: Serve from cache, fall back to network self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request) .then((cached) => cached || fetch(event.request)) .catch(() => caches.match('/offline.html')) ); });

Registration // main.js if ('serviceWorker' in navigator) { window.addEventListener('load', async () => { try { const registration = await navigator.serviceWorker.register('/sw.js', { scope: '/' }); console.log('SW registered:', registration.scope); } catch (error) { console.error('SW registration failed:', error); } }); }

Caching Strategies Strategy Selection Guide Strategy Use Case Description Cache First Static assets (CSS, JS, images) Check cache, fall back to network Network First API responses, dynamic content Try network, fall back to cache Stale While Revalidate Semi-static content (avatars, articles) Serve cache immediately, update in background Network Only Non-cacheable requests (analytics) Always use network Cache Only Offline-only assets Only serve from cache Cache First (Offline First) // Best for: Static assets that rarely change self.addEventListener('fetch', (event) => { if (event.request.destination === 'image' || event.request.destination === 'style' || event.request.destination === 'script') { event.respondWith( caches.match(event.request) .then((cached) => { if (cached) return cached; return fetch(event.request).then((response) => { const clone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(event.request, clone); }); return response; }); }) ); } });

Network First (Fresh First) // Best for: API data, frequently updated content self.addEventListener('fetch', (event) => { if (event.request.url.includes('/api/')) { event.respondWith( fetch(event.request) .then((response) => { const clone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(event.request, clone); }); return response; }) .catch(() => caches.match(event.request)) ); } });

Stale While Revalidate // Best for: Content that's okay to be slightly outdated self.addEventListener('fetch', (event) => { if (event.request.url.includes('/articles/')) { event.respondWith( caches.open(CACHE_NAME).then((cache) => { return cache.match(event.request).then((cached) => { const fetchPromise = fetch(event.request).then((response) => { cache.put(event.request, response.clone()); return response; }); return cached || fetchPromise; }); }) ); } });

Workbox (Recommended) Why Workbox? Battle-tested caching strategies Precaching with revision management Background sync for offline forms Automatic cache cleanup TypeScript support Installation npm install workbox-webpack-plugin # Webpack npm install @vite-pwa/vite-plugin # Vite

Workbox with Vite // vite.config.js import { VitePWA } from 'vite-plugin-pwa';

export default { plugins: [ VitePWA({ registerType: 'autoUpdate', includeAssets: ['favicon.ico', 'robots.txt', 'apple-touch-icon.png'], manifest: { name: 'My App', short_name: 'App', theme_color: '#ffffff', icons: [ { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' }, { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' } ] }, workbox: { globPatterns: ['/.{js,css,html,ico,png,svg}'], runtimeCaching: [ { urlPattern: /^https:\/\/api.example.com\/./i, handler: 'NetworkFirst', options: { cacheName: 'api-cache', expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 // 24 hours } } }, { urlPattern: /.(?:png|jpg|jpeg|svg|gif)$/, handler: 'CacheFirst', options: { cacheName: 'image-cache', expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days } } } ] } }) ] };

Workbox Manual Service Worker // sw.js import { precacheAndRoute } from 'workbox-precaching'; import { registerRoute } from 'workbox-routing'; import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'; import { ExpirationPlugin } from 'workbox-expiration'; import { CacheableResponsePlugin } from 'workbox-cacheable-response';

// Precache static assets (generated by build tool) precacheAndRoute(self.__WB_MANIFEST);

// Cache images registerRoute( ({ request }) => request.destination === 'image', new CacheFirst({ cacheName: 'images', plugins: [ new CacheableResponsePlugin({ statuses: [0, 200] }), new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 // 30 days }) ] }) );

// Cache API responses registerRoute( ({ url }) => url.pathname.startsWith('/api/'), new NetworkFirst({ cacheName: 'api-responses', plugins: [ new CacheableResponsePlugin({ statuses: [0, 200] }), new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 24 * 60 * 60 // 24 hours }) ] }) );

// Cache page navigations registerRoute( ({ request }) => request.mode === 'navigate', new NetworkFirst({ cacheName: 'pages', plugins: [ new CacheableResponsePlugin({ statuses: [0, 200] }) ] }) );

Offline Experience Offline Page

Offline - App Name
📡

You're offline

Check your connection and try again.

Offline Detection // Online/offline status handling function updateOnlineStatus() { const status = navigator.onLine ? 'online' : 'offline'; document.body.dataset.connectionStatus = status;

if (!navigator.onLine) { showNotification('You are offline. Some features may be unavailable.'); } }

window.addEventListener('online', updateOnlineStatus); window.addEventListener('offline', updateOnlineStatus); updateOnlineStatus();

Background Sync (Queue Offline Actions) // sw.js with Workbox import { BackgroundSyncPlugin } from 'workbox-background-sync'; import { registerRoute } from 'workbox-routing'; import { NetworkOnly } from 'workbox-strategies';

const bgSyncPlugin = new BackgroundSyncPlugin('formQueue', { maxRetentionTime: 24 * 60 // Retry for 24 hours });

registerRoute( ({ url }) => url.pathname === '/api/submit', new NetworkOnly({ plugins: [bgSyncPlugin] }), 'POST' );

// main.js - Queue form submission async function submitForm(data) { try { const response = await fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return response.json(); } catch (error) { // Will be retried by background sync when online showNotification('Saved offline. Will sync when connected.'); } }

App-Like Features Install Prompt let deferredPrompt;

window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; showInstallButton(); });

async function installApp() { if (!deferredPrompt) return;

deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice;

console.log(User ${outcome === 'accepted' ? 'accepted' : 'dismissed'} install); deferredPrompt = null; hideInstallButton(); }

window.addEventListener('appinstalled', () => { console.log('App installed'); deferredPrompt = null; });

Detecting Standalone Mode // Check if running as installed PWA function isInstalledPWA() { return window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true; // iOS }

// Listen for display mode changes window.matchMedia('(display-mode: standalone)') .addEventListener('change', (e) => { console.log('Display mode:', e.matches ? 'standalone' : 'browser'); });

Push Notifications // Request permission async function requestNotificationPermission() { const permission = await Notification.requestPermission(); if (permission === 'granted') { await subscribeToPush(); } return permission; }

// Subscribe to push async function subscribeToPush() { const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY) });

// Send subscription to server await fetch('/api/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(subscription) }); }

// sw.js - Handle push events self.addEventListener('push', (event) => { const data = event.data.json(); event.waitUntil( self.registration.showNotification(data.title, { body: data.body, icon: '/icons/icon-192.png', badge: '/icons/badge-72.png', data: { url: data.url } }) ); });

// Handle notification click self.addEventListener('notificationclick', (event) => { event.notification.close(); event.waitUntil( clients.openWindow(event.notification.data.url) ); });

Share Target // sw.js - Handle share target self.addEventListener('fetch', (event) => { if (event.request.url.endsWith('/share') && event.request.method === 'POST') { event.respondWith((async () => { const formData = await event.request.formData(); const title = formData.get('title'); const text = formData.get('text'); const url = formData.get('url');

  // Store or process shared content
  // Redirect to app with shared data
  return Response.redirect(`/?shared=true&title=${encodeURIComponent(title)}`);
})());

} });

Performance Optimization Critical Rendering Path

Image Optimization

<img src="/images/hero-800.webp" srcset=" /images/hero-400.webp 400w, /images/hero-800.webp 800w, /images/hero-1200.webp 1200w " sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px" alt="Hero image" loading="lazy" decoding="async"

Hero image

Code Splitting // Dynamic imports for route-based splitting const routes = { '/': () => import('./pages/Home.js'), '/about': () => import('./pages/About.js'), '/settings': () => import('./pages/Settings.js') };

async function loadPage(path) { const loader = routes[path]; if (loader) { const module = await loader(); return module.default; } }

Testing PWA Lighthouse Audit

Run Lighthouse from CLI

npx lighthouse https://your-app.com --view

Key metrics to check:

- PWA badge (installable, offline-ready)

- Performance score

- Best practices

- Accessibility

Manual Testing Checklist

Installability

Install prompt appears on desktop Chrome Can be added to home screen on mobile App opens in standalone mode after install

Offline Support

App loads when offline (airplane mode) Cached pages display correctly Offline fallback page shows for uncached routes Background sync works when coming back online

Performance

First Contentful Paint < 1.8s Largest Contentful Paint < 2.5s Time to Interactive < 3.8s Cumulative Layout Shift < 0.1

Service Worker

SW registers successfully Static assets cached on install SW updates correctly (new version) No stale cache issues

Manifest

All required fields present Icons display correctly Theme color applied Splash screen shows on launch Testing Service Worker Updates // Force update check if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then((registration) => { registration.update(); }); }

// Listen for updates navigator.serviceWorker.addEventListener('controllerchange', () => { // New service worker activated window.location.reload(); });

Project Structure project/ ├── public/ │ ├── manifest.json # Web app manifest │ ├── sw.js # Service worker (if not bundled) │ ├── offline.html # Offline fallback page │ ├── robots.txt │ └── icons/ │ ├── icon-72.png │ ├── icon-96.png │ ├── icon-128.png │ ├── icon-144.png │ ├── icon-152.png │ ├── icon-192.png │ ├── icon-384.png │ ├── icon-512.png │ ├── icon-maskable.png # For adaptive icons │ ├── apple-touch-icon.png │ └── favicon.ico ├── src/ │ ├── sw.js # Service worker source (if bundled) │ ├── pwa/ │ │ ├── install.js # Install prompt handling │ │ ├── offline.js # Offline detection │ │ └── push.js # Push notification handling │ └── ... └── tests/ └── pwa/ ├── manifest.test.js ├── sw.test.js └── offline.test.js

Common Mistakes Mistake Fix Missing maskable icon Add icon with "purpose": "maskable" No offline fallback Create offline.html and cache it Cache never expires Use ExpirationPlugin with Workbox SW caches too aggressively Use appropriate strategies per resource type No update mechanism Implement skipWaiting() + reload prompt Broken install prompt Ensure manifest meets all criteria No HTTPS in production Configure SSL certificate Large cache size Set maxEntries and maxAgeSeconds Stale API responses Use NetworkFirst for dynamic data Missing start_url tracking Add query param: /?source=pwa PWA Development Checklist Before Launch HTTPS configured (production) Manifest complete with all required fields Icons in all required sizes (192, 512, maskable) Service worker registered and working Offline page created and cached Cache strategies defined for all resource types Install prompt handling implemented Lighthouse PWA audit passes After Launch Monitor cache sizes Test SW updates don't break app Track PWA installs via analytics Test on multiple devices/browsers Monitor Core Web Vitals Set up push notification flow (if needed) Framework-Specific Guides Next.js npm install next-pwa

// next.config.js const withPWA = require('next-pwa')({ dest: 'public', disable: process.env.NODE_ENV === 'development' });

module.exports = withPWA({ // Your Next.js config });

Create React App

CRA 4+ has PWA support built-in

npx create-react-app my-pwa --template cra-template-pwa

Vite (Any Framework) npm install vite-plugin-pwa -D

See Workbox with Vite section above for configuration.

Quick Reference Caching Strategy Cheat Sheet Static Assets (CSS, JS, images) → Cache First API Responses → Network First User-generated content → Stale While Revalidate Analytics, non-cacheable → Network Only Offline-only assets → Cache Only

Manifest Minimum Requirements { "name": "App Name", "short_name": "App", "start_url": "/", "display": "standalone", "icons": [ { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" } ] }

Service Worker Lifecycle 1. Register → 2. Install → 3. Activate → 4. Fetch ↓ ↓ ↓ ↓ Load app Cache assets Clean old Serve requests caches from cache/network

返回排行榜