乐闻世界logo
搜索文章和话题

如何在 Service Worker 中实现推送通知功能?

3月7日 12:06

Service Worker 推送通知实现详解

推送通知是 Service Worker 的重要功能之一,允许服务器向用户发送消息,即使用户没有打开网站。

推送通知架构

shell
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 服务器 │────▶│ 推送服务 │────▶│ 浏览器 │ (Web App) │ │(FCM/APNs等) │ │(Service Worker) └─────────────┘ └─────────────┘ └─────────────┘

实现步骤

1. 请求通知权限

javascript
// main.js async 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. 订阅推送服务

javascript
// main.js async 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 接收推送

javascript
// 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. 处理通知点击

javascript
// 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 库)

javascript
// server.js const 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. 定时推送(定期后台同步)

javascript
// 主线程请求定期同步 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. 富媒体通知

javascript
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. 取消订阅

javascript
// main.js async 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('已取消推送订阅'); } }

浏览器兼容性

功能ChromeFirefoxSafariEdge
Push API✅ (16.4+)
Notification
Actions
Badge

最佳实践

  1. 尊重用户:不要频繁发送通知,提供取消订阅选项
  2. 及时更新:定期清理无效的订阅
  3. 个性化内容:根据用户偏好发送相关通知
  4. 优雅降级:不支持推送的浏览器提供替代方案
  5. HTTPS 必需:推送功能必须在 HTTPS 环境下运行
标签:Service Worker