Building Your Learning Module...
Getting things ready for you!
Find videos you like?
Save to resource drawer for future reference!
Service Workers are JavaScript files that run in the background, separate from your web page. They act as a programmable network proxy, allowing you to intercept network requests, cache resources, and provide offline functionality. They're the foundation of Progressive Web Apps (PWAs).
Cache resources and serve them when offline
Defer actions until network connectivity
Receive notifications when app isn't open
Browser downloads and parses the service worker file
Service worker installs and caches essential resources
Service worker takes control and cleans up old caches
Service worker intercepts requests or terminates when idle
// Check if service workers are supported
if ('serviceWorker' in navigator) {
// Register after page load
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('Service Worker registered:', registration.scope);
// Check for updates
registration.update();
// Listen for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
console.log('New service worker found');
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// New update available
console.log('New content available, refresh to update');
} else {
// First install
console.log('Content cached for offline use');
}
}
});
});
})
.catch(error => {
console.error('Service Worker registration failed:', error);
});
});
// Listen for controller change (when new SW activates)
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('Service Worker updated, reloading...');
window.location.reload();
});
} else {
console.log('Service Workers not supported');
}// service-worker.js
const CACHE_NAME = 'my-app-v1';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/logo.png',
'/offline.html'
];
// Install event - cache assets
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(ASSETS_TO_CACHE);
})
.then(() => {
// Take control immediately
return self.skipWaiting();
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('Service Worker activating...');
event.waitUntil(
caches.keys()
.then(cacheNames => {
return Promise.all(
cacheNames
.filter(cacheName => cacheName !== CACHE_NAME)
.map(cacheName => {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
})
);
})
.then(() => {
// Take control of all pages
return self.clients.claim();
})
);
});// STRATEGY 1: Cache First (best for static assets)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(cachedResponse => {
// Return cached version or fetch from network
return cachedResponse || fetch(event.request);
})
);
});
// STRATEGY 2: Network First (best for dynamic content)
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then(networkResponse => {
// Cache the fresh response
return caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
})
.catch(() => {
// Fallback to cache if network fails
return caches.match(event.request);
})
);
});
// STRATEGY 3: Stale While Revalidate (best for balance)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(cachedResponse => {
// Fetch in background and update cache
const fetchPromise = fetch(event.request)
.then(networkResponse => {
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
});
// Return cached immediately, or wait for network
return cachedResponse || fetchPromise;
})
);
});
// STRATEGY 4: Cache with Network Fallback + Offline Page
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request)
.catch(() => {
// Show offline page for navigation requests
if (event.request.mode === 'navigate') {
return caches.match('/offline.html');
}
});
})
);
});
// STRATEGY 5: Selective Caching (different strategies per resource)
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Cache first for static assets
if (url.pathname.match(/\.(css|js|png|jpg|gif|svg)$/)) {
event.respondWith(
caches.match(event.request)
.then(cached => cached || fetch(event.request))
);
}
// Network first for API calls
else if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(event.request)
.catch(() => caches.match(event.request))
);
}
// Network only for everything else
else {
event.respondWith(fetch(event.request));
}
});// Main thread - register sync
navigator.serviceWorker.ready.then(registration => {
// Register a sync event
return registration.sync.register('sync-messages');
}).catch(error => {
console.error('Sync registration failed:', error);
});
// Service Worker - handle sync event
self.addEventListener('sync', (event) => {
console.log('Background sync:', event.tag);
if (event.tag === 'sync-messages') {
event.waitUntil(
// Get pending messages from IndexedDB
getPendingMessages()
.then(messages => {
// Send all pending messages
return Promise.all(
messages.map(msg =>
fetch('/api/messages', {
method: 'POST',
body: JSON.stringify(msg)
})
)
);
})
.then(() => {
// Clear pending messages
return clearPendingMessages();
})
.catch(error => {
console.error('Sync failed:', error);
throw error; // Retry later
})
);
}
});
// Example: Queue message for sync
async function queueMessage(message) {
// Save to IndexedDB
await saveToPendingMessages(message);
// Register sync
if ('serviceWorker' in navigator && 'sync' in ServiceWorkerRegistration.prototype) {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-messages');
} else {
// Fallback: send immediately
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify(message)
});
}
}// Main thread - subscribe to push
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
// Request notification permission
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
console.log('Notification permission denied');
return;
}
// Subscribe to push notifications
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
});
// Send subscription to server
await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
console.log('Subscribed to push notifications');
}
// Service Worker - handle push event
self.addEventListener('push', (event) => {
console.log('Push notification received');
let data = {
title: 'New Notification',
body: 'You have a new message',
icon: '/icon.png',
badge: '/badge.png'
};
if (event.data) {
data = event.data.json();
}
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon,
badge: data.badge,
data: data.url,
actions: [
{ action: 'open', title: 'Open' },
{ action: 'close', title: 'Close' }
]
})
);
});
// Handle notification click
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'open') {
// Open the app
event.waitUntil(
clients.openWindow(event.notification.data || '/')
);
}
});
// Helper 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;
}// service-worker.js - Complete PWA implementation
const CACHE_NAME = 'pwa-cache-v1';
const RUNTIME_CACHE = 'pwa-runtime-v1';
const PRECACHE_URLS = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/manifest.json',
'/offline.html'
];
// Install - precache essential assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(PRECACHE_URLS))
.then(() => self.skipWaiting())
);
});
// Activate - cleanup old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME && name !== RUNTIME_CACHE)
.map(name => caches.delete(name))
);
})
.then(() => self.clients.claim())
);
});
// Fetch - smart caching strategy
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Skip non-GET requests
if (request.method !== 'GET') return;
// Handle API requests
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request));
return;
}
// Handle navigation requests
if (request.mode === 'navigate') {
event.respondWith(
fetch(request)
.catch(() => caches.match('/offline.html'))
);
return;
}
// Handle static assets
event.respondWith(cacheFirst(request));
});
// Cache first strategy
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
try {
const response = await fetch(request);
const cache = await caches.open(RUNTIME_CACHE);
cache.put(request, response.clone());
return response;
} catch (error) {
console.error('Fetch failed:', error);
throw error;
}
}
// Network first strategy
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open(RUNTIME_CACHE);
cache.put(request, response.clone());
return response;
} catch (error) {
const cached = await caches.match(request);
if (cached) return cached;
throw error;
}
}
// Background sync
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-data') {
event.waitUntil(syncData());
}
});
async function syncData() {
// Get pending data from IndexedDB
const db = await openDB();
const pending = await db.getAll('pending');
// Sync each item
for (const item of pending) {
try {
await fetch('/api/sync', {
method: 'POST',
body: JSON.stringify(item)
});
await db.delete('pending', item.id);
} catch (error) {
console.error('Sync failed for item:', item.id);
}
}
}
// Push notifications
self.addEventListener('push', (event) => {
const data = event.data ? event.data.json() : {};
event.waitUntil(
self.registration.showNotification(data.title || 'Notification', {
body: data.body,
icon: data.icon || '/icon-192.png',
badge: '/badge-72.png',
data: { url: data.url },
actions: [
{ action: 'open', title: 'Open' },
{ action: 'close', title: 'Dismiss' }
]
})
);
});
// Notification click
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'open' || !event.action) {
event.waitUntil(
clients.openWindow(event.notification.data.url || '/')
);
}
});
// Message handling (communicate with main thread)
self.addEventListener('message', (event) => {
if (event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data.type === 'CLEAR_CACHE') {
event.waitUntil(
caches.keys()
.then(names => Promise.all(names.map(name => caches.delete(name))))
);
}
});Cache resources for offline access
Intercept and control network requests
Defer actions until connectivity
Essential for Progressive Web Apps