Service Workers for Offline Web Apps
Service Workers for offline functionality, background sync, and network intercepting
Key Facts
- Category
- Web Technologies
- Items
- 4
- Format Families
- sample
Sample Overview
Service Workers for offline functionality, background sync, and network intercepting This sample set belongs to Web Technologies and can be used to test related workflows inside Elysia Tools.
💻 Basic Service Worker Setup javascript
🟢 simple
⭐⭐
Register a service worker and enable offline caching
⏱️ 15 min
🏷️ service worker, offline, cache, pwa
Prerequisites:
JavaScript, Basic web development
// Basic Service Worker Setup
// service-worker.js
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/icon.png'
];
// Install event - cache files
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
// Fetch event - serve from cache when offline
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;
});
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', event => {
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);
}
})
);
})
);
});
// main.js - Register service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
// Check for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New content is available, show update notification
if (confirm('New version available! Reload to update?')) {
window.location.reload();
}
}
});
});
})
.catch(err => {
console.log('ServiceWorker registration failed: ', err);
});
});
// Listen for controlling service worker
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('ServiceWorker controller changed');
window.location.reload();
});
}
💻 Background Synchronization javascript
🟡 intermediate
⭐⭐⭐
Sync data when network connection is restored
⏱️ 25 min
🏷️ background sync, offline, pwa, indexeddb
Prerequisites:
JavaScript, Service Workers, IndexedDB
// Background Sync Service Worker
// service-worker.js
self.addEventListener('sync', event => {
if (event.tag === 'background-sync') {
event.waitUntil(syncData());
}
});
async function syncData() {
try {
// Get all pending sync requests from IndexedDB
const pendingRequests = await getPendingRequests();
for (const request of pendingRequests) {
try {
const response = await fetch(request.url, {
method: request.method,
headers: request.headers,
body: request.body
});
if (response.ok) {
// Remove successful request from pending
await removePendingRequest(request.id);
console.log('Sync successful for request:', request.id);
} else {
console.error('Sync failed for request:', request.id);
}
} catch (error) {
console.error('Network error during sync:', error);
throw error; // This will retry the sync
}
}
// Show notification if sync completed
if (await getPendingRequestsCount() === 0) {
self.registration.showNotification('Sync Complete', {
body: 'All your data has been synchronized',
icon: '/sync-icon.png'
});
}
} catch (error) {
console.error('Background sync failed:', error);
}
}
// IndexedDB helpers for pending requests
async function getPendingRequests() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('sync-db', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['requests'], 'readonly');
const store = transaction.objectStore('requests');
const getAll = store.getAll();
getAll.onsuccess = () => resolve(getAll.result);
getAll.onerror = () => reject(getAll.error);
};
request.onupgradeneeded = () => {
const db = request.result;
db.createObjectStore('requests', { keyPath: 'id' });
};
});
}
async function removePendingRequest(id) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('sync-db', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['requests'], 'readwrite');
const store = transaction.objectStore('requests');
const deleteRequest = store.delete(id);
deleteRequest.onsuccess = () => resolve();
deleteRequest.onerror = () => reject(deleteRequest.error);
};
});
}
// main.js - Register background sync
class BackgroundSyncManager {
constructor() {
this.registration = null;
}
async init() {
this.registration = await navigator.serviceWorker.ready;
}
async registerSync() {
if ('serviceWorker' in navigator &&
typeof ServiceWorkerRegistration !== 'undefined' &&
'sync' in ServiceWorkerRegistration.prototype) {
try {
await this.registration.sync.register('background-sync');
console.log('Background sync registered');
return true;
} catch (error) {
console.error('Background sync registration failed:', error);
return false;
}
} else {
console.warn('Background sync not supported');
return false;
}
}
async addPendingRequest(requestData) {
const request = {
id: Date.now().toString(),
...requestData,
timestamp: Date.now()
};
// Save to IndexedDB
const db = await this.openDB();
const transaction = db.transaction(['requests'], 'readwrite');
const store = transaction.objectStore('requests');
await store.add(request);
// Try to register background sync
const syncRegistered = await this.registerSync();
if (!syncRegistered) {
// Fallback: try immediate sync
this.tryImmediateSync(request);
}
return request.id;
}
async openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('sync-db', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = () => {
const db = request.result;
db.createObjectStore('requests', { keyPath: 'id' });
};
});
}
async tryImmediateSync(request) {
try {
const response = await fetch(request.url, {
method: request.method,
headers: request.headers,
body: request.body
});
if (response.ok) {
await this.removePendingRequest(request.id);
console.log('Immediate sync successful');
} else {
console.log('Immediate sync failed, keeping in pending queue');
}
} catch (error) {
console.log('Network unavailable, keeping in pending queue');
}
}
}
// Usage example
const syncManager = new BackgroundSyncManager();
// Initialize when app loads
window.addEventListener('load', async () => {
await syncManager.init();
});
// Example: Save data with background sync
async function saveDataOffline(data) {
const requestData = {
url: '/api/save',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
};
const requestId = await syncManager.addPendingRequest(requestData);
// Show user that data will be synced
alert('Your data will be synced when you're back online');
}
💻 Push Notifications javascript
🟡 intermediate
⭐⭐⭐⭐
Receive and handle push notifications with service workers
⏱️ 30 min
🏷️ push notifications, pwa, service worker, messaging
Prerequisites:
JavaScript, Service Workers, HTTPS setup
// Push Notifications with Service Worker
// service-worker.js
self.addEventListener('push', event => {
const options = {
body: event.data ? event.data.text() : 'New notification',
icon: '/notification-icon.png',
badge: '/notification-badge.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'explore',
title: 'Explore this new world',
icon: '/images/checkmark.png'
},
{
action: 'close',
title: 'Close notification',
icon: '/images/xmark.png'
}
]
};
event.waitUntil(
self.registration.showNotification('Push Notification', options)
);
});
// Handle notification clicks
self.addEventListener('notificationclick', event => {
event.notification.close();
if (event.action === 'explore') {
// Open the app to specific page
event.waitUntil(
clients.openWindow('/explore')
);
} else if (event.action === 'close') {
// Just close the notification
console.log('Notification closed');
} else {
// Default action - open main app
event.waitUntil(
clients.matchAll().then(clientList => {
for (const client of clientList) {
if (client.url === '/' && 'focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow('/');
}
})
);
}
});
// Handle notification close
self.addEventListener('notificationclose', event => {
console.log('Notification closed:', event.notification);
});
// main.js - Push notification subscription
class PushNotificationManager {
constructor() {
this.subscription = null;
this.publicKey = 'YOUR_VAPID_PUBLIC_KEY';
}
async init() {
// Check for service worker support
if (!('serviceWorker' in navigator)) {
console.error('Service Worker not supported');
return false;
}
// Check for push notification support
if (!('PushManager' in window)) {
console.error('Push notifications not supported');
return false;
}
// Register service worker
const registration = await navigator.serviceWorker.register('/push-service-worker.js');
console.log('Service Worker registered');
this.registration = registration;
return true;
}
async subscribeToPush() {
try {
// Subscribe to push notifications
this.subscription = await this.registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlBase64ToUint8Array(this.publicKey)
});
console.log('User is subscribed:', this.subscription);
// Send subscription to server
await this.sendSubscriptionToServer(this.subscription);
return this.subscription;
} catch (error) {
console.error('Failed to subscribe to push notifications:', error);
return null;
}
}
async unsubscribeFromPush() {
try {
const result = await this.subscription.unsubscribe();
console.log('Unsubscribed:', result);
// Remove subscription from server
await this.removeSubscriptionFromServer(this.subscription);
this.subscription = null;
return result;
} catch (error) {
console.error('Failed to unsubscribe:', error);
return false;
}
}
async sendSubscriptionToServer(subscription) {
try {
const response = await fetch('/api/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
});
if (!response.ok) {
throw new Error('Failed to send subscription to server');
}
return await response.json();
} catch (error) {
console.error('Error sending subscription to server:', error);
throw error;
}
}
async removeSubscriptionFromServer(subscription) {
try {
const response = await fetch('/api/unsubscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
});
if (!response.ok) {
throw new Error('Failed to remove subscription from server');
}
} catch (error) {
console.error('Error removing subscription from server:', error);
throw error;
}
}
async requestNotificationPermission() {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
console.log('Notification permission granted');
return true;
} else {
console.log('Notification permission denied');
return false;
}
}
// Helper function to convert VAPID key
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;
}
async checkSubscriptionStatus() {
this.subscription = await this.registration.pushManager.getSubscription();
if (this.subscription) {
console.log('User is already subscribed');
return this.subscription;
} else {
console.log('User is not subscribed');
return null;
}
}
}
// Usage example
const pushManager = new PushNotificationManager();
// Initialize push notifications
async function initPushNotifications() {
const initialized = await pushManager.init();
if (!initialized) return;
// Request permission
const permissionGranted = await pushManager.requestNotificationPermission();
if (!permissionGranted) return;
// Check if already subscribed
const existingSubscription = await pushManager.checkSubscriptionStatus();
if (!existingSubscription) {
// Subscribe to push notifications
await pushManager.subscribeToPush();
}
}
// Subscribe button handler
document.getElementById('subscribe-btn').addEventListener('click', async () => {
await initPushNotifications();
});
// Unsubscribe button handler
document.getElementById('unsubscribe-btn').addEventListener('click', async () => {
await pushManager.unsubscribeFromPush();
});
// Test local notification
document.getElementById('test-notification').addEventListener('click', async () => {
if (Notification.permission === 'granted') {
new Notification('Test Notification', {
body: 'This is a test notification!',
icon: '/icon.png'
});
} else {
console.log('Notification permission not granted');
}
});
💻 Advanced Caching Strategies javascript
🟡 intermediate
⭐⭐⭐⭐
Implement different caching strategies for optimal performance
⏱️ 35 min
🏷️ caching, performance, offline, pwa, strategies
Prerequisites:
JavaScript, Service Workers, Performance optimization
// Advanced Service Worker Caching Strategies
// service-worker.js
// Different cache strategies
class CacheStrategies {
// Cache First - try cache first, fallback to network
static cacheFirst(request) {
return caches.match(request)
.then(response => {
return response || fetch(request);
});
}
// Network First - try network first, fallback to cache
static networkFirst(request) {
return fetch(request)
.then(response => {
// Cache successful responses
if (response.status === 200) {
const responseClone = response.clone();
caches.open('dynamic-cache-v1')
.then(cache => cache.put(request, responseClone));
}
return response;
})
.catch(() => {
// Fallback to cache
return caches.match(request);
});
}
// Stale While Revalidate - serve from cache, update in background
static staleWhileRevalidate(request) {
const cachePromise = caches.match(request);
const networkPromise = fetch(request)
.then(response => {
const responseClone = response.clone();
caches.open('dynamic-cache-v1')
.then(cache => cache.put(request, responseClone));
return response;
});
return cachePromise
.then(response => response || networkPromise)
.catch(() => networkPromise);
}
// Network Only - always fetch from network
static networkOnly(request) {
return fetch(request);
}
// Cache Only - always serve from cache
static cacheOnly(request) {
return caches.match(request);
}
}
// Cache management
class CacheManager {
constructor() {
this.caches = {
static: 'static-cache-v1',
dynamic: 'dynamic-cache-v1',
api: 'api-cache-v1',
images: 'images-cache-v1'
};
this.cacheRules = [
// Static assets - cache first
{
match: request =>
request.destination === 'script' ||
request.destination === 'style' ||
request.destination === 'font',
strategy: 'cacheFirst',
cacheName: this.caches.static
},
// Images - stale while revalidate
{
match: request => request.destination === 'image',
strategy: 'staleWhileRevalidate',
cacheName: this.caches.images,
maxEntries: 100,
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
},
// API calls - network first with timeout
{
match: request => request.url.includes('/api/'),
strategy: 'networkFirst',
cacheName: this.caches.api,
timeout: 3000, // 3 seconds
maxEntries: 50
},
// HTML pages - network first
{
match: request =>
request.destination === 'document' ||
request.mode === 'navigate',
strategy: 'networkFirst',
cacheName: this.caches.dynamic
},
// External CDN - cache first
{
match: request =>
request.url.includes('cdn.jsdelivr.net') ||
request.url.includes('cdnjs.cloudflare.com'),
strategy: 'cacheFirst',
cacheName: this.caches.static
}
];
}
async handleRequest(request) {
const rule = this.findMatchingRule(request);
if (!rule) {
return fetch(request);
}
try {
// Apply strategy with timeout if specified
if (rule.timeout) {
return await this.withTimeout(
CacheStrategies[rule.strategy](request),
rule.timeout,
caches.match(request)
);
} else {
const response = await CacheStrategies[rule.strategy](request);
// Apply cache management rules
if (response && response.ok && rule.cacheName) {
await this.manageCache(rule.cacheName, request, rule);
}
return response;
}
} catch (error) {
console.error('Cache strategy failed:', error);
// Fallback to cache if available
return caches.match(request) || new Response('Offline', {
status: 503,
statusText: 'Service Unavailable'
});
}
}
findMatchingRule(request) {
return this.cacheRules.find(rule => rule.match(request));
}
async withTimeout(promise, timeout, fallback) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]).catch(error => {
if (fallback) return fallback;
throw error;
});
}
async manageCache(cacheName, request, rule) {
if (rule.maxEntries || rule.maxAge) {
const cache = await caches.open(cacheName);
const requests = await cache.keys();
// Remove old entries if maxEntries exceeded
if (rule.maxEntries && requests.length > rule.maxEntries) {
const toDelete = requests.slice(0, requests.length - rule.maxEntries);
await Promise.all(toDelete.map(req => cache.delete(req)));
}
// Remove old entries based on age
if (rule.maxAge) {
const now = Date.now();
for (const cacheRequest of requests) {
const response = await cache.match(cacheRequest);
const dateHeader = response.headers.get('date');
if (dateHeader && (now - new Date(dateHeader).getTime()) > rule.maxAge) {
cache.delete(cacheRequest);
}
}
}
}
}
}
// Initialize cache manager
const cacheManager = new CacheManager();
// Service worker event handlers
self.addEventListener('fetch', event => {
event.respondWith(cacheManager.handleRequest(event.request));
});
// Install event - precache critical assets
self.addEventListener('install', event => {
const staticAssets = [
'/',
'/index.html',
'/offline.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png'
];
event.waitUntil(
caches.open(cacheManager.caches.static)
.then(cache => cache.addAll(staticAssets))
);
});
// Activate event - clean up old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => !Object.values(cacheManager.caches).includes(name))
.map(name => caches.delete(name))
);
})
);
});
// Background sync for offline analytics
self.addEventListener('sync', event => {
if (event.tag === 'analytics-sync') {
event.waitUntil(syncAnalytics());
}
});
async function syncAnalytics() {
const offlineData = await getOfflineAnalytics();
for (const data of offlineData) {
try {
await fetch('/api/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
await removeOfflineAnalytics(data.id);
} catch (error) {
console.error('Failed to sync analytics:', error);
break;
}
}
}
// IndexedDB helpers for offline analytics
async function getOfflineAnalytics() {
// Implementation for getting analytics from IndexedDB
return [];
}
async function removeOfflineAnalytics(id) {
// Implementation for removing analytics from IndexedDB
}
// main.js - Client-side analytics that works offline
class OfflineAnalytics {
constructor() {
this.db = null;
}
async init() {
this.db = await this.openDB();
}
async openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('analytics-db', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = () => {
const db = request.result;
db.createObjectStore('events', { keyPath: 'id', autoIncrement: true });
};
});
}
async track(eventName, properties = {}) {
const event = {
name: eventName,
properties: {
...properties,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent
}
};
try {
// Try to send immediately
const response = await fetch('/api/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
if (!response.ok) throw new Error('Analytics request failed');
} catch (error) {
// Store offline
await this.storeOffline(event);
// Register background sync if available
if ('serviceWorker' in navigator &&
typeof ServiceWorkerRegistration !== 'undefined' &&
'sync' in ServiceWorkerRegistration.prototype) {
const registration = await navigator.serviceWorker.ready;
registration.sync.register('analytics-sync');
}
}
}
async storeOffline(event) {
const transaction = this.db.transaction(['events'], 'readwrite');
const store = transaction.objectStore('events');
await store.add(event);
}
}
// Usage
const analytics = new OfflineAnalytics();
analytics.init();
// Track page view
analytics.track('page_view', {
page: window.location.pathname,
title: document.title
});
// Track user interaction
document.getElementById('cta-button').addEventListener('click', () => {
analytics.track('cta_clicked', {
button_text: 'Get Started',
location: 'homepage'
});
});