Service Worker PWA Grundlagen
Service Worker & PWA: Web-Apps wie native Apps
Progressive Web Apps (PWAs) bringen native App-Features ins Web. Offline-Support, Push-Benachrichtigungen und Installation – alles mit Service Workern möglich.
Was ist ein Service Worker?
Ein Service Worker ist ein JavaScript, das im Hintergrund läuft – getrennt von der Webseite. Er kann:
- Netzwerk-Anfragen abfangen und cachen
- Offline-Funktionalität ermöglichen
- Push-Benachrichtigungen empfangen
- Hintergrund-Synchronisation durchführen
Service Worker registrieren
// main.js - Service Worker registrieren
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registriert:', registration.scope);
})
.catch(error => {
console.log('SW Registrierung fehlgeschlagen:', error);
});
});
}
Service Worker Lebenszyklus
// sw.js - Service Worker
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
'/',
'/styles.css',
'/script.js',
'/offline.html'
];
// 1. Install Event - Dateien cachen
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Cache geöffnet');
return cache.addAll(urlsToCache);
})
);
});
// 2. Activate Event - Alte Caches löschen
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
console.log('Alter Cache gelöscht:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
});
// 3. Fetch Event - Anfragen abfangen
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache hit - return response
if (response) {
return response;
}
// Nicht im Cache - Netzwerk anfragen
return fetch(event.request);
})
);
});
Caching-Strategien
Cache First (Offline First)
// Erst Cache, dann Netzwerk
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
Network First
// Erst Netzwerk, Cache als Fallback
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request)
.then(response => {
// Erfolgreiche Antwort cachen
const responseClone = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => caches.match(event.request))
);
});
Stale While Revalidate
// Cache sofort liefern, im Hintergrund aktualisieren
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;
});
})
);
});
Web App Manifest
Das Manifest macht aus der Website eine installierbare PWA:
// manifest.json
{
"name": "Meine App",
"short_name": "App",
"description": "Eine tolle Progressive Web App",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3b82f6",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/icons/icon-512-maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
Manifest einbinden
<!-- Im <head> --> <link rel="manifest" href="/manifest.json"> <meta name="theme-color" content="#3b82f6"> <!-- iOS-spezifisch --> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="default"> <link rel="apple-touch-icon" href="/icons/icon-192.png">
Offline-Seite
// sw.js - Offline Fallback
self.addEventListener('fetch', event => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.catch(() => caches.match('/offline.html'))
);
}
});
Push-Benachrichtigungen
Berechtigung anfragen
// main.js
async function subscribeToPush() {
const permission = await Notification.requestPermission();
if (permission !== 'granted') return;
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
});
// Subscription an Server senden
await fetch('/api/push-subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: { 'Content-Type': 'application/json' }
});
}
Push empfangen
// sw.js
self.addEventListener('push', event => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge.png',
vibrate: [100, 50, 100],
data: { url: data.url }
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// Klick auf Benachrichtigung
self.addEventListener('notificationclick', event => {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});
Workbox (Google Library)
Vereinfacht Service Worker Entwicklung:
// sw.js mit Workbox
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-sw.js');
// Cache First für Bilder
workbox.routing.registerRoute(
({request}) => request.destination === 'image',
new workbox.strategies.CacheFirst({
cacheName: 'images',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Tage
}),
],
})
);
// Network First für API
workbox.routing.registerRoute(
({url}) => url.pathname.startsWith('/api/'),
new workbox.strategies.NetworkFirst({
cacheName: 'api-cache',
})
);
// Stale While Revalidate für CSS/JS
workbox.routing.registerRoute(
({request}) => request.destination === 'style' ||
request.destination === 'script',
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'static-resources',
})
);
PWA Checkliste
✅ PWA Requirements:
- HTTPS (außer localhost)
- Web App Manifest
- Service Worker
- Icons (min. 192x192 und 512x512)
- Responsive Design
- Offline-Funktionalität