5月31日 17:42
How does PWA implement update mechanism? How to ensure users use the latest version?
PWA update mechanism is very important for ensuring users always use the latest version of the app. Here's the complete PWA update process and best practices:
Service Worker Update Process
1. Update Detection
Browsers check for Service Worker updates in the following situations:
- When navigating to the app page
- When Service Worker events are triggered (such as push, sync, etc.)
- Automatic check every 24 hours
2. Update Lifecycle
javascript// sw.js const CACHE_VERSION = 'v2'; const CACHE_NAME = `my-pwa-${CACHE_VERSION}`; // Install event self.addEventListener('install', event => { console.log('Installing new Service Worker:', CACHE_VERSION); event.waitUntil( caches.open(CACHE_NAME) .then(cache => { return cache.addAll([ '/', '/index.html', '/styles/main.css', '/scripts/app.js', '/images/logo.png' ]); }) .then(() => { // Skip waiting, activate immediately return self.skipWaiting(); }) ); }); // Activate event self.addEventListener('activate', event => { console.log('Activating new Service Worker:', CACHE_VERSION); event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { // Delete old version caches if (cacheName.startsWith('my-pwa-') && cacheName !== CACHE_NAME) { console.log('Deleting old cache:', cacheName); return caches.delete(cacheName); } }) ); }).then(() => { // Immediately control all clients return self.clients.claim(); }) ); });
3. Notify Users of Updates
javascript// Listen for updates in main thread let newWorker; if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') .then(registration => { // Check if there's a new Service Worker registration.addEventListener('updatefound', () => { newWorker = registration.installing; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { // New Service Worker available showUpdateNotification(); } }); }); }); } // Show update notification function showUpdateNotification() { const notification = document.createElement('div'); notification.innerHTML = ` <div class="update-notification"> <span>New version available</span> <button id="update-btn">Update Now</button> <button id="dismiss-btn">Later</button> </div> `; notification.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: #007bff; color: white; padding: 15px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 9999; font-family: Arial, sans-serif; `; document.body.appendChild(notification); // Update now button document.getElementById('update-btn').addEventListener('click', () => { newWorker.postMessage({ action: 'skipWaiting' }); window.location.reload(); }); // Later button document.getElementById('dismiss-btn').addEventListener('click', () => { notification.remove(); }); }
Manual Update Trigger
javascript// Manually check for updates async function checkForUpdates() { if ('serviceWorker' in navigator) { const registration = await navigator.serviceWorker.getRegistration(); if (registration) { await registration.update(); console.log('Checked for updates'); } } } // Periodically check for updates (every hour) setInterval(checkForUpdates, 60 * 60 * 1000); // Check for updates when page gets focus window.addEventListener('focus', checkForUpdates);
Cache Update Strategy
1. Versioned Caches
javascript// Use version numbers to manage caches const CACHE_VERSIONS = { static: 'v1', dynamic: 'v1', images: 'v1' }; const CACHE_NAMES = { static: `static-${CACHE_VERSIONS.static}`, dynamic: `dynamic-${CACHE_VERSIONS.dynamic}`, images: `images-${CACHE_VERSIONS.images}` }; // Update specific cache type function updateCacheType(type) { CACHE_VERSIONS[type] = 'v' + (parseInt(CACHE_VERSIONS[type].slice(1)) + 1); CACHE_NAMES[type] = `${type}-${CACHE_VERSIONS[type]}`; }
2. Smart Cache Update
javascriptself.addEventListener('fetch', event => { const url = new URL(event.request.url); // For HTML documents, always fetch latest version from network if (event.request.mode === 'navigate') { event.respondWith( fetch(event.request) .then(response => { const responseClone = response.clone(); caches.open(CACHE_NAMES.dynamic).then(cache => { cache.put(event.request, responseClone); }); return response; }) .catch(() => caches.match(event.request)) ); } // For static resources, use cache first else if (url.pathname.match(/\.(css|js|png|jpg|jpeg|gif|svg|woff|woff2)$/)) { event.respondWith(cacheFirst(event.request)); } // For API requests, use network first else if (url.pathname.startsWith('/api/')) { event.respondWith(networkFirst(event.request)); } });
Pre-cache Update
javascript// Pre-cache critical resources during install self.addEventListener('install', event => { const CRITICAL_ASSETS = [ '/', '/index.html', '/styles/main.css', '/scripts/app.js', '/offline.html' ]; event.waitUntil( caches.open(CACHE_NAMES.static) .then(cache => { return cache.addAll(CRITICAL_ASSETS); }) ); }); // Update pre-cache during activate self.addEventListener('activate', event => { event.waitUntil( caches.open(CACHE_NAMES.static) .then(cache => { return cache.addAll([ '/styles/main.css', '/scripts/app.js' ]); }) ); });
Background Sync Update
javascript// Register background sync self.addEventListener('sync', event => { if (event.tag === 'sync-updates') { event.waitUntil(syncUpdates()); } }); async function syncUpdates() { try { // Get latest resource list const response = await fetch('/api/updates'); const updates = await response.json(); // Update cache const cache = await caches.open(CACHE_NAMES.dynamic); for (const update of updates) { await cache.add(update.url); } console.log('Background sync completed'); } catch (error) { console.error('Background sync failed:', error); } } // Request background sync in main thread async function requestBackgroundSync() { const registration = await navigator.serviceWorker.ready; await registration.sync.register('sync-updates'); }
Update Strategy Selection
1. Immediate Update
javascript// Force immediate update function forceUpdate() { if ('serviceWorker' in navigator) { navigator.serviceWorker.getRegistration().then(registration => { if (registration && registration.waiting) { registration.waiting.postMessage({ action: 'skipWaiting' }); } }); } }
2. Delayed Update
javascript// Update when user is idle function updateWhenIdle() { if ('serviceWorker' in navigator) { navigator.serviceWorker.getRegistration().then(registration => { if (registration) { registration.update(); } }); } } // Use requestIdleCallback window.requestIdleCallback(updateWhenIdle);
3. Smart Update
javascript// Decide update strategy based on network conditions function smartUpdate() { if ('connection' in navigator) { const connection = navigator.connection; // Update on Wi-Fi or fast network if (connection.effectiveType === '4g' || connection.type === 'wifi') { checkForUpdates(); } // Delay update on slow network else { setTimeout(checkForUpdates, 60000); // Update after 1 minute } } }
Update Best Practices
1. Version Management
javascript// Use semantic versioning const VERSION = { major: 1, minor: 2, patch: 3 }; const CACHE_VERSION = `v${VERSION.major}.${VERSION.minor}.${VERSION.patch}`; // Increment version function incrementVersion(type) { if (type === 'major') { VERSION.major++; VERSION.minor = 0; VERSION.patch = 0; } else if (type === 'minor') { VERSION.minor++; VERSION.patch = 0; } else { VERSION.patch++; } }
2. Rollback Mechanism
javascript// Keep old version caches const MAX_CACHE_VERSIONS = 3; self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { // Get all version numbers const versions = cacheNames .filter(name => name.startsWith('my-pwa-')) .map(name => name.replace('my-pwa-', '')) .sort() .reverse(); // Delete old versions, keep recent versions const versionsToDelete = versions.slice(MAX_CACHE_VERSIONS); return Promise.all( versionsToDelete.map(version => { return caches.delete(`my-pwa-${version}`); }) ); }) ); });
3. Update Notification
javascript// Provide detailed update information function showDetailedUpdateNotification(updateInfo) { const notification = document.createElement('div'); notification.innerHTML = ` <div class="update-notification"> <h3>New Version Available</h3> <p>Version: ${updateInfo.version}</p> <p>Changes:</p> <ul> ${updateInfo.changes.map(change => `<li>${change}</li>`).join('')} </ul> <button id="update-btn">Update Now</button> <button id="dismiss-btn">Later</button> </div> `; document.body.appendChild(notification); document.getElementById('update-btn').addEventListener('click', () => { forceUpdate(); window.location.reload(); }); document.getElementById('dismiss-btn').addEventListener('click', () => { notification.remove(); }); }
Monitoring and Debugging
1. Update Logging
javascript// Log update events function logUpdateEvent(event) { const logData = { timestamp: Date.now(), event: event.type, version: CACHE_VERSION, userAgent: navigator.userAgent }; // Send to server fetch('/api/update-log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(logData) }); } // Listen to Service Worker events self.addEventListener('install', logUpdateEvent); self.addEventListener('activate', logUpdateEvent);
2. Debug Tools
javascript// Add debug information if (location.hostname === 'localhost') { self.addEventListener('install', event => { console.log('[SW] Installing:', CACHE_VERSION); }); self.addEventListener('activate', event => { console.log('[SW] Activating:', CACHE_VERSION); }); self.addEventListener('fetch', event => { console.log('[SW] Fetch:', event.request.url); }); }
Summary
Key points for PWA updates:
- Version Management: Use version numbers to manage caches
- Update Detection: Periodically check for Service Worker updates
- User Notification: Notify users promptly when new version is available
- Smooth Updates: Provide good update experience
- Rollback Mechanism: Keep old versions for rollback
- Smart Strategy: Choose update strategy based on network conditions
- Monitoring Logs: Record update events for debugging
- Testing Validation: Test update process under different conditions