PWA 的推送通知功能使用 Web Push API 和 Service Worker 实现,能够在用户未打开应用时发送通知。以下是完整的实现方案:
推送通知的核心组件
1. Push API
用于接收服务器推送的消息
2. Notification API
用于显示通知
3. Service Worker
在后台处理推送事件
实现推送通知的步骤
步骤 1:生成 VAPID 密钥
VAPID(Voluntary Application Server Identification)用于验证推送服务器的身份。
bash# 使用 web-push 生成密钥 npm install -g web-push web-push generate-vapid-keys
生成结果:
shellPublic Key: <your-public-key> Private Key: <your-private-key>
步骤 2:请求推送订阅权限
javascript// 在主线程中请求订阅 async function subscribeUser() { // 检查浏览器支持 if (!('serviceWorker' in navigator) || !('PushManager' in window)) { console.log('Push messaging is not supported'); return; } try { // 获取 Service Worker 注册 const registration = await navigator.serviceWorker.ready; // 请求订阅 const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array('<your-public-key>') }); console.log('User is subscribed:', subscription); // 将订阅信息发送到服务器 await saveSubscriptionToServer(subscription); } catch (error) { console.log('Failed to subscribe the user:', error); } } // 将 Base64 字符串转换为 Uint8Array 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; }
步骤 3:在 Service Worker 中处理推送事件
javascript// sw.js self.addEventListener('push', event => { console.log('Push event received:', event); let data = { title: '新消息', body: '您有一条新消息', icon: '/icons/icon-192x192.png', badge: '/icons/badge-72x72.png' }; // 解析推送数据 if (event.data) { try { data = { ...data, ...event.data.json() }; } catch (error) { data.body = event.data.text(); } } // 显示通知 const options = { body: data.body, icon: data.icon, badge: data.badge, vibrate: [200, 100, 200], data: { dateOfArrival: Date.now(), primaryKey: 1 }, actions: [ { action: 'explore', title: '查看详情', icon: '/icons/explore.png' }, { action: 'close', title: '关闭', icon: '/icons/close.png' } ] }; event.waitUntil( self.registration.showNotification(data.title, options) ); });
步骤 4:处理通知点击事件
javascriptself.addEventListener('notificationclick', event => { console.log('Notification click received:', event); event.notification.close(); // 处理不同的操作 if (event.action === 'explore') { // 打开特定页面 event.waitUntil( clients.openWindow('/details') ); } else if (event.action === 'close') { // 关闭通知,不做其他操作 return; } else { // 默认操作:打开应用首页 event.waitUntil( clients.matchAll({ type: 'window' }).then(clientList => { // 如果已有打开的窗口,聚焦到它 for (const client of clientList) { if (client.url === '/' && 'focus' in client) { return client.focus(); } } // 否则打开新窗口 if (clients.openWindow) { return clients.openWindow('/'); } }) ); } });
步骤 5:服务器端发送推送消息
使用 Node.js 和 web-push 库:
javascriptconst webpush = require('web-push'); // 设置 VAPID 密钥 const vapidKeys = { publicKey: '<your-public-key>', privateKey: '<your-private-key>' }; webpush.setVapidDetails( 'mailto:your-email@example.com', vapidKeys.publicKey, vapidKeys.privateKey ); // 发送推送消息 async function sendPushNotification(subscription, data) { try { await webpush.sendNotification(subscription, JSON.stringify(data)); console.log('Push notification sent successfully'); } catch (error) { console.error('Error sending push notification:', error); // 如果订阅无效,从数据库中删除 if (error.statusCode === 410) { await removeSubscriptionFromDatabase(subscription); } } } // 示例:向所有订阅者发送通知 async function sendToAllSubscribers(message) { const subscriptions = await getAllSubscriptionsFromDatabase(); const promises = subscriptions.map(subscription => { return sendPushNotification(subscription, { title: '新通知', body: message, icon: '/icons/icon-192x192.png' }); }); await Promise.allSettled(promises); }
推送通知的高级功能
1. 静默推送
javascriptself.addEventListener('push', event => { if (!event.data) return; const data = event.data.json(); // 如果是静默推送,不显示通知 if (data.silent) { event.waitUntil( // 执行后台任务,如同步数据 syncData() ); return; } // 否则显示通知 event.waitUntil( self.registration.showNotification(data.title, { body: data.body, icon: data.icon }) ); });
2. 定时推送
javascript// 使用 setTimeout 实现延迟推送 self.addEventListener('push', event => { const data = event.data.json(); if (data.delay) { setTimeout(() => { self.registration.showNotification(data.title, { body: data.body }); }, data.delay); } else { self.registration.showNotification(data.title, { body: data.body }); } });
3. 富媒体通知
javascriptself.addEventListener('push', event => { const data = event.data.json(); const options = { body: data.body, icon: data.icon, image: data.image, // 大图片 badge: data.badge, // 小图标 vibrate: [200, 100, 200], sound: '/sounds/notification.mp3', tag: 'unique-tag', // 用于替换相同标签的通知 renotify: true, // 重复通知时提醒用户 requireInteraction: true, // 需要用户交互才能关闭 actions: [ { action: 'reply', title: '回复', icon: '/icons/reply.png', type: 'text', placeholder: '输入回复内容' } ], data: { // 自定义数据 url: data.url } }; event.waitUntil( self.registration.showNotification(data.title, options) ); });
推送通知的最佳实践
- 请求权限的时机:在用户有明确需求时请求,而不是页面加载时
- 通知内容:提供有价值的信息,避免垃圾通知
- 频率控制:不要过于频繁发送通知
- 个性化:根据用户偏好定制通知内容
- 可操作性:提供有用的操作按钮
- 错误处理:妥善处理订阅失效的情况
- 测试:在不同设备和浏览器上测试通知效果
浏览器兼容性
- Chrome、Edge、Firefox:完全支持
- Safari:部分支持(iOS 16.4+)
- 需要用户授权
调试推送通知
使用 Chrome DevTools:
- 打开 Application 面板
- 查看 Service Workers 标签
- 点击 "Push" 按钮模拟推送
- 查看通知显示效果