// service-worker.js
const CACHE_NAME = 'my-pwa-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/manifest.json',
'/icon-192.png',
'/icon-512.png',
];
// Install event - cache resources
self.addEventListener('install', (event) => {
console.log('Service Worker installing');
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Caching app shell');
return cache.addAll(urlsToCache);
})
.then(() => self.skipWaiting()) // Activate immediately
);
});
// Activate event - clean old caches
self.addEventListener('activate', (event) => {
console.log('Service Worker activating');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
}).then(() => self.clients.claim()) // Take control of all pages
);
});
// Fetch event - serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Cache hit - return response
if (response) {
return response;
}
// Clone the request
const fetchRequest = event.request.clone();
return fetch(fetchRequest).then((response) => {
// Check if valid response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone the response
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
});
})
.catch(() => {
// Return offline page if available
return caches.match('/offline.html');
})
);
});
// Background sync
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-messages') {
event.waitUntil(syncMessages());
}
});
async function syncMessages() {
const db = await openDB();
const messages = await db.getAll('outbox');
for (const message of messages) {
try {
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify(message),
});
await db.delete('outbox', message.id);
} catch (error) {
console.error('Sync failed:', error);
}
}
}
// Push notifications
self.addEventListener('push', (event) => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/icon-192.png',
badge: '/badge-72.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: data.id,
},
actions: [
{ action: 'explore', title: 'View' },
{ action: 'close', title: 'Close' },
],
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// Notification click
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'explore') {
event.waitUntil(
clients.openWindow('/notifications')
);
}
});
// Caching strategies
// 1. Cache First (good for static assets)
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image') {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request).then((response) => {
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, response.clone());
return response;
});
});
})
);
}
});
// 2. Network First (good for API calls)
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then((response) => {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => caches.match(event.request))
);
}
});
// 3. Stale While Revalidate (good for frequently updated content)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then((cache) => {
return cache.match(event.request).then((response) => {
const fetchPromise = fetch(event.request).then((networkResponse) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return response || fetchPromise;
});
})
);
});
// Register service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('SW registered:', registration.scope);
// Check for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New version available
if (confirm('New version available! Reload?')) {
window.location.reload();
}
}
});
});
} catch (error) {
console.error('SW registration failed:', error);
}
});
}
// Install prompt
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
// Show install button
const installBtn = document.getElementById('install-btn');
installBtn.style.display = 'block';
installBtn.addEventListener('click', async () => {
installBtn.style.display = 'none';
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`User response: ${outcome}`);
deferredPrompt = null;
});
});
// Push notifications
async function subscribeToPushNotifications() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY),
});
await fetch('/api/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);
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
}
// Background sync
async function sendMessage(message) {
if ('serviceWorker' in navigator && 'SyncManager' in window) {
const registration = await navigator.serviceWorker.ready;
// Save to IndexedDB
await saveToOutbox(message);
// Register sync
await registration.sync.register('sync-messages');
} else {
// Fallback: send immediately
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify(message),
});
}
}
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "A comprehensive PWA example",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4285f4",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"categories": ["productivity", "utilities"],
"screenshots": [
{
"src": "/screenshots/desktop.png",
"sizes": "1280x720",
"type": "image/png"
},
{
"src": "/screenshots/mobile.png",
"sizes": "750x1334",
"type": "image/png"
}
],
"shortcuts": [
{
"name": "New Message",
"short_name": "New",
"description": "Create a new message",
"url": "/new",
"icons": [{ "src": "/icons/new.png", "sizes": "192x192" }]
}
]
}
Progressive Web Apps (PWAs) combine web and native app features. I register service workers to intercept network requests and enable offline functionality. The service worker lifecycle includes install, activate, and fetch events. Cache API stores assets for offline access with different caching strategies. The workbox library simplifies service worker development with precaching and runtime caching. The web app manifest (manifest.json) defines app metadata, icons, and display mode. Install prompts let users add PWAs to home screens. Background sync queues requests when offline. Push notifications engage users. PWAs provide app-like experiences with web technology.