Service Worker Push Notification Implementation
Push notifications are one of the important features of Service Worker, allowing servers to send messages to users even when they haven't opened the website.
Push Notification Architecture
shell┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Server │────▶│ Push Service│────▶│ Browser │ │ (Web App) │ │(FCM/APNs) │ │(Service Worker) └─────────────┘ └─────────────┘ └─────────────┘
Implementation Steps
1. Request Notification Permission
javascript// main.js async function requestNotificationPermission() { const permission = await Notification.requestPermission(); if (permission === 'granted') { console.log('Notification permission granted'); await subscribeUserToPush(); } else if (permission === 'denied') { console.log('Notification permission denied'); } else { console.log('Notification permission pending'); } } // Check current permission status console.log('Current permission:', Notification.permission); // 'default' | 'granted' | 'denied'
2. Subscribe to Push Service
javascript// main.js async function subscribeUserToPush() { const registration = await navigator.serviceWorker.ready; // Get or create subscription let subscription = await registration.pushManager.getSubscription(); if (!subscription) { // Get VAPID public key from server const vapidPublicKey = await fetch('/api/vapid-public-key').then(r => r.text()); // Convert to Uint8Array const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); // Create subscription subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, // Must be set to true applicationServerKey: convertedVapidKey }); console.log('Push subscription successful:', subscription); } // Send subscription info to server await fetch('/api/save-subscription', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(subscription) }); return subscription; } // VAPID public key conversion function 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 Receives Push
javascript// sw.js // Listen for push events self.addEventListener('push', event => { console.log('Push message received:', event); let data = {}; if (event.data) { data = event.data.json(); } const options = { body: data.body || 'You have a new message', icon: '/icons/icon-192x192.png', badge: '/icons/badge-72x72.png', image: data.image || '/images/notification-banner.jpg', tag: data.tag || 'default', requireInteraction: false, // Keep notification until user interaction actions: [ { action: 'open', title: 'Open', icon: '/icons/open.png' }, { action: 'dismiss', title: 'Dismiss', icon: '/icons/close.png' } ], data: { url: data.url || '/', timestamp: Date.now() } }; event.waitUntil( self.registration.showNotification(data.title || 'New Notification', options) ); });
4. Handle Notification Click
javascript// sw.js // Listen for notification click events self.addEventListener('notificationclick', event => { console.log('Notification clicked:', event); event.notification.close(); const { action, notification } = event; const data = notification.data || {}; if (action === 'dismiss') { // User clicked dismiss button return; } // Default behavior or click open button event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }) .then(clientList => { const urlToOpen = data.url || '/'; // Check if window is already open for (const client of clientList) { if (client.url === urlToOpen && 'focus' in client) { return client.focus(); } } // Open new window if not if (clients.openWindow) { return clients.openWindow(urlToOpen); } }) ); }); // Listen for notification close events self.addEventListener('notificationclose', event => { console.log('Notification closed:', event); // Can record statistics here });
Server-Side Push Sending
Node.js Example (using web-push library)
javascript// server.js const webpush = require('web-push'); // Set VAPID keys const vapidKeys = { publicKey: 'YOUR_PUBLIC_KEY', privateKey: 'YOUR_PRIVATE_KEY' }; webpush.setVapidDetails( 'mailto:your-email@example.com', vapidKeys.publicKey, vapidKeys.privateKey ); // Store subscription info (use database in production) let subscriptions = []; // Save subscription app.post('/api/save-subscription', (req, res) => { const subscription = req.body; subscriptions.push(subscription); res.json({ success: true }); }); // Send push 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)) ); // Clean invalid subscriptions results.forEach((result, index) => { if (result.status === 'rejected' && result.reason.statusCode === 410) { subscriptions.splice(index, 1); } }); res.json({ success: true, sent: results.length }); });
Advanced Features
1. Scheduled Push (Periodic Background Sync)
javascript// Main thread requests periodic sync navigator.serviceWorker.ready.then(registration => { registration.periodicSync.register('daily-news', { minInterval: 24 * 60 * 60 * 1000 // 24 hours }); }); // sw.js listens self.addEventListener('periodicsync', event => { if (event.tag === 'daily-news') { event.waitUntil(showDailyNewsNotification()); } });
2. Rich Media Notifications
javascriptconst options = { body: 'Click to view details', icon: '/icon.png', image: '/banner.jpg', // Large image badge: '/badge.png', // Small icon vibrate: [200, 100, 200], // Vibration pattern sound: '/notification.mp3', // Sound (partial browser support) dir: 'ltr', // Text direction lang: 'en-US', timestamp: Date.now(), requireInteraction: true, actions: [ { action: 'reply', title: 'Reply', icon: '/reply.png' }, { action: 'archive', title: 'Archive', icon: '/archive.png' } ] };
3. Unsubscribe
javascript// main.js async function unsubscribeFromPush() { const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.getSubscription(); if (subscription) { await subscription.unsubscribe(); // Notify server to delete subscription await fetch('/api/remove-subscription', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ endpoint: subscription.endpoint }) }); console.log('Push subscription cancelled'); } }
Browser Compatibility
| Feature | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| Push API | ✅ | ✅ | ✅ (16.4+) | ✅ |
| Notification | ✅ | ✅ | ✅ | ✅ |
| Actions | ✅ | ✅ | ❌ | ✅ |
| Badge | ✅ | ❌ | ❌ | ✅ |
Best Practices
- Respect Users: Don't send notifications frequently, provide unsubscribe option
- Update Regularly: Clean invalid subscriptions periodically
- Personalized Content: Send relevant notifications based on user preferences
- Graceful Degradation: Provide alternative for browsers without push support
- HTTPS Required: Push functionality must run in HTTPS environment