如何在 Service Worker 中实现推送通知功能?
Service Worker 推送通知实现详解推送通知是 Service Worker 的重要功能之一,允许服务器向用户发送消息,即使用户没有打开网站。推送通知架构┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ 服务器 │────▶│ 推送服务 │────▶│ 浏览器 ││ (Web App) │ │(FCM/APNs等) │ │(Service Worker)└─────────────┘ └─────────────┘ └─────────────┘实现步骤1. 请求通知权限// main.jsasync function requestNotificationPermission() { const permission = await Notification.requestPermission(); if (permission === 'granted') { console.log('通知权限已授予'); await subscribeUserToPush(); } else if (permission === 'denied') { console.log('通知权限被拒绝'); } else { console.log('通知权限待处理'); }}// 检查当前权限状态console.log('当前权限:', Notification.permission);// 'default' | 'granted' | 'denied'2. 订阅推送服务// main.jsasync function subscribeUserToPush() { const registration = await navigator.serviceWorker.ready; // 获取或创建订阅 let subscription = await registration.pushManager.getSubscription(); if (!subscription) { // 从服务器获取 VAPID 公钥 const vapidPublicKey = await fetch('/api/vapid-public-key').then(r => r.text()); // 转换为 Uint8Array const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); // 创建订阅 subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, // 必须设置为 true applicationServerKey: convertedVapidKey }); console.log('推送订阅成功:', subscription); } // 将订阅信息发送到服务器 await fetch('/api/save-subscription', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(subscription) }); return subscription;}// VAPID 公钥转换函数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 接收推送// sw.js// 监听推送事件self.addEventListener('push', event => { console.log('收到推送消息:', event); let data = {}; if (event.data) { data = event.data.json(); } const options = { body: data.body || '您有一条新消息', icon: '/icons/icon-192x192.png', badge: '/icons/badge-72x72.png', image: data.image || '/images/notification-banner.jpg', tag: data.tag || 'default', requireInteraction: false, // 是否保持通知直到用户交互 actions: [ { action: 'open', title: '打开', icon: '/icons/open.png' }, { action: 'dismiss', title: '忽略', icon: '/icons/close.png' } ], data: { url: data.url || '/', timestamp: Date.now() } }; event.waitUntil( self.registration.showNotification(data.title || '新通知', options) );});4. 处理通知点击// sw.js// 监听通知点击事件self.addEventListener('notificationclick', event => { console.log('通知被点击:', event); event.notification.close(); const { action, notification } = event; const data = notification.data || {}; if (action === 'dismiss') { // 用户点击忽略按钮 return; } // 默认行为或点击打开按钮 event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }) .then(clientList => { const urlToOpen = data.url || '/'; // 检查是否已有窗口打开 for (const client of clientList) { if (client.url === urlToOpen && 'focus' in client) { return client.focus(); } } // 没有则打开新窗口 if (clients.openWindow) { return clients.openWindow(urlToOpen); } }) );});// 监听通知关闭事件self.addEventListener('notificationclose', event => { console.log('通知被关闭:', event); // 可以在这里记录统计数据});服务器端发送推送Node.js 示例(使用 web-push 库)// server.jsconst 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);// 存储订阅信息(生产环境应使用数据库)let subscriptions = [];// 保存订阅app.post('/api/save-subscription', (req, res) => { const subscription = req.body; subscriptions.push(subscription); res.json({ success: true });});// 发送推送app.post('/api/send-push', async (req, res) => { const { title, body, url } = req.body; const payload = JSON.stringify({ title, body, url, icon: '/icons/icon-192x192.png' }); const results = await Promise.allSettled( subscriptions.map(sub => webpush.sendNotification(sub, payload)) ); // 清理无效订阅 results.forEach((result, index) => { if (result.status === 'rejected' && result.reason.statusCode === 410) { subscriptions.splice(index, 1); } }); res.json({ success: true, sent: results.length });});高级功能1. 定时推送(定期后台同步)// 主线程请求定期同步navigator.serviceWorker.ready.then(registration => { registration.periodicSync.register('daily-news', { minInterval: 24 * 60 * 60 * 1000 // 24小时 });});// sw.js 监听self.addEventListener('periodicsync', event => { if (event.tag === 'daily-news') { event.waitUntil(showDailyNewsNotification()); }});2. 富媒体通知const options = { body: '点击查看详情', icon: '/icon.png', image: '/banner.jpg', // 大图 badge: '/badge.png', // 小图标 vibrate: [200, 100, 200], // 振动模式 sound: '/notification.mp3', // 声音(部分浏览器支持) dir: 'ltr', // 文字方向 lang: 'zh-CN', timestamp: Date.now(), requireInteraction: true, actions: [ { action: 'reply', title: '回复', icon: '/reply.png' }, { action: 'archive', title: '归档', icon: '/archive.png' } ]};3. 取消订阅// main.jsasync function unsubscribeFromPush() { const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.getSubscription(); if (subscription) { await subscription.unsubscribe(); // 通知服务器删除订阅 await fetch('/api/remove-subscription', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ endpoint: subscription.endpoint }) }); console.log('已取消推送订阅'); }}浏览器兼容性| 功能 | Chrome | Firefox | Safari | Edge ||------|--------|---------|--------|------|| Push API | ✅ | ✅ | ✅ (16.4+) | ✅ || Notification | ✅ | ✅ | ✅ | ✅ || Actions | ✅ | ✅ | ❌ | ✅ || Badge | ✅ | ❌ | ❌ | ✅ |最佳实践尊重用户:不要频繁发送通知,提供取消订阅选项及时更新:定期清理无效的订阅个性化内容:根据用户偏好发送相关通知优雅降级:不支持推送的浏览器提供替代方案HTTPS 必需:推送功能必须在 HTTPS 环境下运行