PWA 的更新机制对于确保用户始终使用最新版本的应用非常重要。以下是 PWA 更新的完整流程和最佳实践:
Service Worker 更新流程
1. 更新检测
浏览器会在以下情况检查 Service Worker 更新:
- 导航到应用页面时
- Service Worker 事件触发时(如 push、sync 等)
- 每 24 小时自动检查一次
2. 更新生命周期
javascript// sw.js const CACHE_VERSION = 'v2'; const CACHE_NAME = `my-pwa-${CACHE_VERSION}`; // 安装事件 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(() => { // 跳过等待,立即激活 return self.skipWaiting(); }) ); }); // 激活事件 self.addEventListener('activate', event => { console.log('Activating new Service Worker:', CACHE_VERSION); event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { // 删除旧版本的缓存 if (cacheName.startsWith('my-pwa-') && cacheName !== CACHE_NAME) { console.log('Deleting old cache:', cacheName); return caches.delete(cacheName); } }) ); }).then(() => { // 立即控制所有客户端 return self.clients.claim(); }) ); });
3. 更新通知用户
javascript// 在主线程中监听更新 let newWorker; if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') .then(registration => { // 检查是否有新的 Service Worker registration.addEventListener('updatefound', () => { newWorker = registration.installing; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { // 有新的 Service Worker 可用 showUpdateNotification(); } }); }); }); } // 显示更新通知 function showUpdateNotification() { const notification = document.createElement('div'); notification.innerHTML = ` <div class="update-notification"> <span>应用有新版本可用</span> <button id="update-btn">立即更新</button> <button id="dismiss-btn">稍后</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); // 立即更新按钮 document.getElementById('update-btn').addEventListener('click', () => { newWorker.postMessage({ action: 'skipWaiting' }); window.location.reload(); }); // 稍后按钮 document.getElementById('dismiss-btn').addEventListener('click', () => { notification.remove(); }); }
手动触发更新
javascript// 手动检查更新 async function checkForUpdates() { if ('serviceWorker' in navigator) { const registration = await navigator.serviceWorker.getRegistration(); if (registration) { await registration.update(); console.log('Checked for updates'); } } } // 定期检查更新(每小时) setInterval(checkForUpdates, 60 * 60 * 1000); // 在页面获得焦点时检查更新 window.addEventListener('focus', checkForUpdates);
缓存更新策略
1. 版本化缓存
javascript// 使用版本号管理缓存 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}` }; // 更新特定类型的缓存 function updateCacheType(type) { CACHE_VERSIONS[type] = 'v' + (parseInt(CACHE_VERSIONS[type].slice(1)) + 1); CACHE_NAMES[type] = `${type}-${CACHE_VERSIONS[type]}`; }
2. 智能缓存更新
javascriptself.addEventListener('fetch', event => { const url = new URL(event.request.url); // 对于 HTML 文档,总是从网络获取最新版本 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)) ); } // 对于静态资源,使用缓存优先 else if (url.pathname.match(/\.(css|js|png|jpg|jpeg|gif|svg|woff|woff2)$/)) { event.respondWith(cacheFirst(event.request)); } // 对于 API 请求,使用网络优先 else if (url.pathname.startsWith('/api/')) { event.respondWith(networkFirst(event.request)); } });
预缓存更新
javascript// 在安装时预缓存关键资源 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); }) ); }); // 在激活时更新预缓存 self.addEventListener('activate', event => { event.waitUntil( caches.open(CACHE_NAMES.static) .then(cache => { return cache.addAll([ '/styles/main.css', '/scripts/app.js' ]); }) ); });
后台同步更新
javascript// 注册后台同步 self.addEventListener('sync', event => { if (event.tag === 'sync-updates') { event.waitUntil(syncUpdates()); } }); async function syncUpdates() { try { // 获取最新的资源列表 const response = await fetch('/api/updates'); const updates = await response.json(); // 更新缓存 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); } } // 在主线程中请求后台同步 async function requestBackgroundSync() { const registration = await navigator.serviceWorker.ready; await registration.sync.register('sync-updates'); }
更新策略选择
1. 立即更新
javascript// 强制立即更新 function forceUpdate() { if ('serviceWorker' in navigator) { navigator.serviceWorker.getRegistration().then(registration => { if (registration && registration.waiting) { registration.waiting.postMessage({ action: 'skipWaiting' }); } }); } }
2. 延迟更新
javascript// 在用户空闲时更新 function updateWhenIdle() { if ('serviceWorker' in navigator) { navigator.serviceWorker.getRegistration().then(registration => { if (registration) { registration.update(); } }); } } // 使用 requestIdleCallback window.requestIdleCallback(updateWhenIdle);
3. 智能更新
javascript// 根据网络条件决定更新策略 function smartUpdate() { if ('connection' in navigator) { const connection = navigator.connection; // 在 Wi-Fi 或快速网络时更新 if (connection.effectiveType === '4g' || connection.type === 'wifi') { checkForUpdates(); } // 在慢速网络时延迟更新 else { setTimeout(checkForUpdates, 60000); // 1分钟后更新 } } }
更新最佳实践
1. 版本管理
javascript// 使用语义化版本号 const VERSION = { major: 1, minor: 2, patch: 3 }; const CACHE_VERSION = `v${VERSION.major}.${VERSION.minor}.${VERSION.patch}`; // 更新版本号 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. 回滚机制
javascript// 保留旧版本缓存 const MAX_CACHE_VERSIONS = 3; self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { // 获取所有版本号 const versions = cacheNames .filter(name => name.startsWith('my-pwa-')) .map(name => name.replace('my-pwa-', '')) .sort() .reverse(); // 删除旧版本,保留最近的几个版本 const versionsToDelete = versions.slice(MAX_CACHE_VERSIONS); return Promise.all( versionsToDelete.map(version => { return caches.delete(`my-pwa-${version}`); }) ); }) ); });
3. 更新通知
javascript// 提供详细的更新信息 function showDetailedUpdateNotification(updateInfo) { const notification = document.createElement('div'); notification.innerHTML = ` <div class="update-notification"> <h3>新版本可用</h3> <p>版本: ${updateInfo.version}</p> <p>更新内容:</p> <ul> ${updateInfo.changes.map(change => `<li>${change}</li>`).join('')} </ul> <button id="update-btn">立即更新</button> <button id="dismiss-btn">稍后</button> </div> `; document.body.appendChild(notification); document.getElementById('update-btn').addEventListener('click', () => { forceUpdate(); window.location.reload(); }); document.getElementById('dismiss-btn').addEventListener('click', () => { notification.remove(); }); }
监控和调试
1. 更新日志
javascript// 记录更新事件 function logUpdateEvent(event) { const logData = { timestamp: Date.now(), event: event.type, version: CACHE_VERSION, userAgent: navigator.userAgent }; // 发送到服务器 fetch('/api/update-log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(logData) }); } // 监听 Service Worker 事件 self.addEventListener('install', logUpdateEvent); self.addEventListener('activate', logUpdateEvent);
2. 调试工具
javascript// 添加调试信息 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); }); }
总结
PWA 更新的关键点:
- 版本管理:使用版本号管理缓存
- 更新检测:定期检查 Service Worker 更新
- 用户通知:及时通知用户有新版本可用
- 平滑更新:提供良好的更新体验
- 回滚机制:保留旧版本以便回滚
- 智能策略:根据网络条件选择更新策略
- 监控日志:记录更新事件便于调试
- 测试验证:在不同条件下测试更新流程