前端 PWA:最佳实践的新方法
前端 PWA最佳实践的新方法一、引言别再忽视 PWAPWA那是原生应用的事儿前端不用管——我相信这是很多前端开发者常说的话。但事实是PWA 可以提供接近原生应用的体验PWA 可以离线访问PWA 可以被安装到主屏幕PWA 可以发送推送通知PWA 可以提高用户 engagementPWA 不是原生应用的专利前端同样可以实现。今天我这个专治用户体验的手艺人就来教你如何实现 PWA提升前端应用的用户体验。二、PWA 的新趋势从简单到全面2.1 现代 PWA 的演进PWA 经历了从简单到全面的演进过程第一代基本 PWAmanifest.json Service Worker第二代高级 PWA离线功能 推送通知第三代渐进式 PWA渐进式增强 性能优化第四代可安装 PWA主屏幕安装 应用体验第五代生态 PWA与原生应用集成 跨平台2.2 PWA 的核心价值PWA 可以带来以下价值离线访问即使在没有网络的情况下也能访问应用可安装可以被安装到主屏幕提供类似原生应用的体验推送通知可以向用户发送推送通知提高用户 engagement性能优化加载速度快响应迅速跨平台可以在不同设备上运行无需为每个平台开发单独的应用三、实战技巧从配置到实现3.1 基本配置!-- 反面教材没有 PWA 配置 -- !-- 没有 manifest.json 和 Service Worker -- !-- 正面教材添加 manifest.json -- !-- public/manifest.json -- { name: My PWA App, short_name: My App, description: A progressive web application, start_url: /, display: standalone, background_color: #ffffff, theme_color: #4285f4, icons: [ { src: icons/icon-72x72.png, sizes: 72x72, type: image/png, purpose: any }, { src: icons/icon-96x96.png, sizes: 96x96, type: image/png, purpose: any }, { src: icons/icon-128x128.png, sizes: 128x128, type: image/png, purpose: any }, { src: icons/icon-144x144.png, sizes: 144x144, type: image/png, purpose: any }, { src: icons/icon-152x152.png, sizes: 152x152, type: image/png, purpose: any }, { src: icons/icon-192x192.png, sizes: 192x192, type: image/png, purpose: any }, { src: icons/icon-384x384.png, sizes: 384x384, type: image/png, purpose: any }, { src: icons/icon-512x512.png, sizes: 512x512, type: image/png, purpose: any } ] } !-- 正面教材2添加 Service Worker -- !-- public/service-worker.js -- const CACHE_NAME my-pwa-cache-v1; const urlsToCache [ /, /index.html, /manifest.json, /icons/icon-72x72.png, /icons/icon-96x96.png, /icons/icon-128x128.png, /icons/icon-144x144.png, /icons/icon-152x152.png, /icons/icon-192x192.png, /icons/icon-384x384.png, /icons/icon-512x512.png, /css/style.css, /js/main.js ]; // 安装 Service Worker self.addEventListener(install, (event) { event.waitUntil( caches.open(CACHE_NAME) .then((cache) { console.log(Opened cache); return cache.addAll(urlsToCache); }) ); }); // 激活 Service Worker self.addEventListener(activate, (event) { const cacheWhitelist [CACHE_NAME]; event.waitUntil( caches.keys().then((cacheNames) { return Promise.all( cacheNames.map((cacheName) { if (cacheWhitelist.indexOf(cacheName) -1) { return caches.delete(cacheName); } }) ); }) ); }); // 拦截网络请求 self.addEventListener(fetch, (event) { event.respondWith( caches.match(event.request) .then((response) { if (response) { return response; } return fetch(event.request).then( (response) { if (!response || response.status ! 200 || response.type ! basic) { return response; } const responseToCache response.clone(); caches.open(CACHE_NAME) .then((cache) { cache.put(event.request, responseToCache); }); return response; } ); }) ); }); !-- 正面教材3注册 Service Worker -- !-- index.html -- !DOCTYPE html html langen head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleMy PWA App/title link relmanifest href/manifest.json meta nametheme-color content#4285f4 link relicon href/icons/icon-192x192.png typeimage/png /head body h1My PWA App/h1 pWelcome to my progressive web application!/p script if (serviceWorker in navigator) { window.addEventListener(load, () { navigator.serviceWorker.register(/service-worker.js) .then((registration) { console.log(Service Worker registered with scope:, registration.scope); }) .catch((error) { console.error(Service Worker registration failed:, error); }); }); } /script /body /html3.2 离线功能// 反面教材没有离线功能 // 依赖网络连接没有缓存策略 // 正面教材实现离线功能 // service-worker.js const CACHE_NAME my-pwa-cache-v1; const urlsToCache [ /, /index.html, /offline.html, /manifest.json, /icons/icon-192x192.png, /css/style.css, /js/main.js ]; // 安装 Service Worker self.addEventListener(install, (event) { event.waitUntil( caches.open(CACHE_NAME) .then((cache) { console.log(Opened cache); return cache.addAll(urlsToCache); }) ); }); // 拦截网络请求 self.addEventListener(fetch, (event) { event.respondWith( caches.match(event.request) .then((response) { if (response) { return response; } return fetch(event.request).then( (response) { if (!response || response.status ! 200 || response.type ! basic) { return response; } const responseToCache response.clone(); caches.open(CACHE_NAME) .then((cache) { cache.put(event.request, responseToCache); }); return response; } ).catch(() { return caches.match(/offline.html); }); }) ); }); // 正面教材2预缓存策略 // service-worker.js const CACHE_NAME my-pwa-cache-v1; const STATIC_CACHE static-cache-v1; const DYNAMIC_CACHE dynamic-cache-v1; const staticUrls [ /, /index.html, /offline.html, /manifest.json, /icons/icon-192x192.png, /css/style.css, /js/main.js ]; // 安装 Service Worker self.addEventListener(install, (event) { event.waitUntil( caches.open(STATIC_CACHE) .then((cache) { console.log(Opened static cache); return cache.addAll(staticUrls); }) ); }); // 激活 Service Worker self.addEventListener(activate, (event) { const cacheWhitelist [STATIC_CACHE, DYNAMIC_CACHE]; event.waitUntil( caches.keys().then((cacheNames) { return Promise.all( cacheNames.map((cacheName) { if (cacheWhitelist.indexOf(cacheName) -1) { return caches.delete(cacheName); } }) ); }) ); }); // 拦截网络请求 self.addEventListener(fetch, (event) { event.respondWith( caches.match(event.request) .then((response) { if (response) { return response; } return fetch(event.request).then( (response) { if (!response || response.status ! 200 || response.type ! basic) { return response; } const responseToCache response.clone(); caches.open(DYNAMIC_CACHE) .then((cache) { cache.put(event.request, responseToCache); }); return response; } ).catch(() { return caches.match(/offline.html); }); }) ); });3.3 推送通知// 反面教材没有推送通知 // 无法与用户进行实时交互 // 正面教材实现推送通知 // service-worker.js // 处理推送事件 self.addEventListener(push, (event) { const data event.data.json(); const options { body: data.body, icon: /icons/icon-192x192.png, badge: /icons/icon-72x72.png, vibrate: [100, 50, 100], data: { url: data.url } }; event.waitUntil( self.registration.showNotification(data.title, options) ); }); // 处理通知点击事件 self.addEventListener(notificationclick, (event) { event.notification.close(); event.waitUntil( clients.openWindow(event.notification.data.url) ); }); // 正面教材2请求推送权限 // main.js async function requestNotificationPermission() { if (Notification in window) { const permission await Notification.requestPermission(); if (permission granted) { console.log(Notification permission granted); await subscribeToPush(); } else { console.log(Notification permission denied); } } } async function subscribeToPush() { if (serviceWorker in navigator PushManager in window) { const registration await navigator.serviceWorker.ready; const subscription await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(YOUR_PUBLIC_VAPID_KEY) }); // 发送订阅信息到服务器 await fetch(/api/push-subscribe, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(subscription) }); } } function urlBase64ToUint8Array(base64String) { const padding .repeat((4 - base64String.length % 4) % 4); const base64 (base64String padding) .replace(/-/g, ) .replace(/_/g, /); const rawData window.atob(base64); const outputArray new Uint8Array(rawData.length); for (let i 0; i rawData.length; i) { outputArray[i] rawData.charCodeAt(i); } return outputArray; } // 调用请求权限函数 document.addEventListener(DOMContentLoaded, requestNotificationPermission);3.4 性能优化// 反面教材没有性能优化 // 加载速度慢用户体验差 // 正面教材性能优化 // service-worker.js const CACHE_NAME my-pwa-cache-v1; const STATIC_CACHE static-cache-v1; const DYNAMIC_CACHE dynamic-cache-v1; const staticUrls [ /, /index.html, /offline.html, /manifest.json, /icons/icon-192x192.png, /css/style.css, /js/main.js ]; // 安装 Service Worker self.addEventListener(install, (event) { self.skipWaiting(); // 强制激活新的 Service Worker event.waitUntil( caches.open(STATIC_CACHE) .then((cache) { console.log(Opened static cache); return cache.addAll(staticUrls); }) ); }); // 激活 Service Worker self.addEventListener(activate, (event) { event.waitUntil( clients.claim() // 立即控制所有客户端 ); const cacheWhitelist [STATIC_CACHE, DYNAMIC_CACHE]; event.waitUntil( caches.keys().then((cacheNames) { return Promise.all( cacheNames.map((cacheName) { if (cacheWhitelist.indexOf(cacheName) -1) { return caches.delete(cacheName); } }) ); }) ); }); // 拦截网络请求 self.addEventListener(fetch, (event) { // 忽略非GET请求 if (event.request.method ! GET) return; // 忽略浏览器扩展请求 if (event.request.url.startsWith(chrome-extension://)) return; // 忽略其他来源的请求 if (!event.request.url.startsWith(self.location.origin)) return; event.respondWith( caches.match(event.request) .then((response) { if (response) { return response; } return fetch(event.request).then( (response) { if (!response || response.status ! 200 || response.type ! basic) { return response; } const responseToCache response.clone(); caches.open(DYNAMIC_CACHE) .then((cache) { cache.put(event.request, responseToCache); }); return response; } ).catch(() { return caches.match(/offline.html); }); }) ); }); // 正面教材2预加载关键资源 // index.html link relpreload href/css/style.css asstyle link relpreload href/js/main.js asscript link relpreload href/icons/icon-192x192.png asimage3.5 可安装性!-- 反面教材没有可安装性 -- !-- 无法被安装到主屏幕 -- !-- 正面教材添加可安装性 -- !-- manifest.json -- { name: My PWA App, short_name: My App, description: A progressive web application, start_url: /, display: standalone, background_color: #ffffff, theme_color: #4285f4, orientation: portrait, categories: [productivity, utilities], icons: [ { src: icons/icon-72x72.png, sizes: 72x72, type: image/png, purpose: any }, { src: icons/icon-96x96.png, sizes: 96x96, type: image/png, purpose: any }, { src: icons/icon-128x128.png, sizes: 128x128, type: image/png, purpose: any }, { src: icons/icon-144x144.png, sizes: 144x144, type: image/png, purpose: any }, { src: icons/icon-152x152.png, sizes: 152x152, type: image/png, purpose: any }, { src: icons/icon-192x192.png, sizes: 192x192, type: image/png, purpose: any }, { src: icons/icon-384x384.png, sizes: 384x384, type: image/png, purpose: any }, { src: icons/icon-512x512.png, sizes: 512x512, type: image/png, purpose: any } ] } !-- 正面教材2处理安装事件 -- !-- main.js -- let deferredPrompt; window.addEventListener(beforeinstallprompt, (e) { // 阻止 Chrome 67 及更早版本自动显示安装提示 e.preventDefault(); // 保存事件以便稍后触发 deferredPrompt e; // 显示自定义安装按钮 document.getElementById(installButton).style.display block; }); document.getElementById(installButton).addEventListener(click, async () { if (!deferredPrompt) return; // 显示安装提示 deferredPrompt.prompt(); // 等待用户响应 const { outcome } await deferredPrompt.userChoice; console.log(User response to install prompt: ${outcome}); // 重置 deferredPrompt因为它只能使用一次 deferredPrompt null; // 隐藏安装按钮 document.getElementById(installButton).style.display none; }); window.addEventListener(appinstalled, (e) { console.log(PWA installed); // 隐藏安装按钮 document.getElementById(installButton).style.display none; });四、PWA 的最佳实践4.1 配置管理manifest.json配置应用的名称、图标、颜色等信息Service Worker实现离线功能、缓存策略等HTTPSPWA 必须使用 HTTPS响应式设计确保应用在不同设备上都能正常显示性能优化优化加载速度和响应时间4.2 离线功能缓存策略使用合适的缓存策略如预缓存、运行时缓存等离线页面提供离线页面当网络不可用时显示数据同步当网络恢复时同步离线数据缓存大小合理设置缓存大小避免占用过多存储空间缓存更新定期更新缓存确保用户获取最新内容4.3 推送通知权限请求合理请求推送通知权限通知内容发送有价值的通知内容通知频率控制通知频率避免打扰用户通知互动支持通知点击等互动操作后台同步使用后台同步 API 同步数据4.4 可安装性安装提示提供自定义安装提示安装条件满足安装条件如 Service Worker、manifest.json 等安装体验优化安装流程提供清晰的安装指引应用图标提供不同尺寸的应用图标启动体验优化应用启动体验减少白屏时间4.5 性能优化加载速度优化资源加载速度使用预加载、预缓存等技术响应时间减少首屏加载时间提高交互响应速度资源优化压缩资源减少资源大小缓存策略使用合适的缓存策略减少网络请求监控监控应用性能及时发现和解决性能问题五、案例分析从 Web 应用到 PWA 的蜕变5.1 问题分析某前端项目存在以下问题离线不可用没有网络时无法访问应用加载速度慢首屏加载时间超过 3 秒用户 engagement 低用户活跃度低回访率低无法安装无法被安装到主屏幕缺乏原生应用体验推送通知无法向用户发送推送通知5.2 解决方案引入 PWA添加 manifest.json 配置实现 Service Worker配置 HTTPS离线功能实现缓存策略提供离线页面实现数据同步推送通知实现推送通知功能合理请求推送权限发送有价值的通知内容可安装性优化安装体验提供不同尺寸的应用图标处理安装事件性能优化优化资源加载速度减少首屏加载时间监控应用性能5.3 效果评估指标优化前优化后改进率离线访问不可用可用100%首屏加载时间3 秒1 秒66.7%用户 engagement低高80%安装率0%30%30%回访率20%60%200%六、常见误区6.1 PWA 的误解PWA 就是离线应用PWA 不仅是离线应用还包括可安装性、推送通知等功能PWA 只能在移动设备上使用PWA 可以在桌面设备上使用PWA 需要复杂的配置PWA 的基本配置相对简单PWA 性能不如原生应用通过优化PWA 的性能可以接近原生应用6.2 常见 PWA 使用错误没有 HTTPSPWA 必须使用 HTTPS缓存策略不合理导致缓存过大或缓存过期推送通知过度频繁发送推送通知打扰用户安装提示时机不当在不合适的时机提示用户安装性能优化不足加载速度慢影响用户体验七、总结PWA 是一种强大的前端技术可以提供接近原生应用的体验。通过合理的配置管理、离线功能、推送通知、可安装性和性能优化你可以构建高质量的 PWA 应用提升用户体验。记住manifest.json配置应用的基本信息Service Worker实现离线功能和缓存策略HTTPS确保应用使用 HTTPS离线功能提供离线访问能力推送通知与用户保持互动可安装性提供类似原生应用的体验性能优化确保应用加载速度快响应迅速别再忽视 PWA现在就开始实现 PWA 吧关于作者钛态cannonmonster01前端 PWA 专家专治各种用户体验问题和离线访问需求。标签前端 PWA、Service Worker、manifest.json、离线功能、推送通知