/** * Service Worker für Webshop System * Offline-Funktionalität und Caching */ const CACHE_NAME = 'webshop-v1'; const STATIC_CACHE = 'webshop-static-v1'; const DYNAMIC_CACHE = 'webshop-dynamic-v1'; // Assets für Offline-Cache const STATIC_ASSETS = [ '/', '/css/bootstrap.min.css', '/css/custom.css', '/js/bootstrap.bundle.min.js', '/js/app.js', '/img/logo.png', '/img/icons/icon-192x192.png', '/img/icons/icon-512x512.png', '/offline.html' ]; // API-Endpoints für Offline-Cache const API_CACHE = [ '/api/products', '/api/categories', '/api/stats' ]; // Install Event - Cache statische Assets self.addEventListener('install', event => { console.log('Service Worker: Installing...'); event.waitUntil( caches.open(STATIC_CACHE) .then(cache => { console.log('Service Worker: Caching static assets'); return cache.addAll(STATIC_ASSETS); }) .then(() => { console.log('Service Worker: Static assets cached'); return self.skipWaiting(); }) .catch(error => { console.error('Service Worker: Error caching static assets', error); }) ); }); // Activate Event - Cleanup alte Caches self.addEventListener('activate', event => { console.log('Service Worker: Activating...'); event.waitUntil( caches.keys() .then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) { console.log('Service Worker: Deleting old cache', cacheName); return caches.delete(cacheName); } }) ); }) .then(() => { console.log('Service Worker: Activated'); return self.clients.claim(); }) ); }); // Fetch Event - Cache-Strategie self.addEventListener('fetch', event => { const { request } = event; const url = new URL(request.url); // Skip non-GET requests if (request.method !== 'GET') { return; } // Skip Chrome extensions if (url.protocol === 'chrome-extension:') { return; } // Cache-Strategie basierend auf URL if (url.pathname.startsWith('/api/')) { // API-Requests: Network-First mit Cache-Fallback event.respondWith(handleApiRequest(request)); } else if (isStaticAsset(url.pathname)) { // Statische Assets: Cache-First event.respondWith(handleStaticAsset(request)); } else { // HTML-Seiten: Network-First mit Cache-Fallback event.respondWith(handlePageRequest(request)); } }); // API-Request Handler async function handleApiRequest(request) { try { // Versuche Network-Request const networkResponse = await fetch(request); if (networkResponse.ok) { // Cache erfolgreiche API-Responses const cache = await caches.open(DYNAMIC_CACHE); cache.put(request, networkResponse.clone()); } return networkResponse; } catch (error) { console.log('Service Worker: Network failed, trying cache', request.url); // Fallback zu Cache const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Offline-Fallback für API-Requests return new Response(JSON.stringify({ error: 'Offline', message: 'Keine Internetverbindung verfügbar' }), { status: 503, headers: { 'Content-Type': 'application/json' } }); } } // Statische Asset Handler async function handleStaticAsset(request) { const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } try { const networkResponse = await fetch(request); if (networkResponse.ok) { const cache = await caches.open(STATIC_CACHE); cache.put(request, networkResponse.clone()); } return networkResponse; } catch (error) { console.log('Service Worker: Static asset not found', request.url); return new Response('Asset not found', { status: 404 }); } } // Page Request Handler async function handlePageRequest(request) { try { // Versuche Network-Request const networkResponse = await fetch(request); if (networkResponse.ok) { // Cache erfolgreiche Page-Responses const cache = await caches.open(DYNAMIC_CACHE); cache.put(request, networkResponse.clone()); } return networkResponse; } catch (error) { console.log('Service Worker: Network failed, trying cache', request.url); // Fallback zu Cache const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Offline-Seite für HTML-Requests if (request.headers.get('accept').includes('text/html')) { return caches.match('/offline.html'); } return new Response('Page not found', { status: 404 }); } } // Prüfe ob es sich um ein statisches Asset handelt function isStaticAsset(pathname) { return pathname.match(/\.(css|js|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$/); } // Background Sync für Offline-Aktionen self.addEventListener('sync', event => { console.log('Service Worker: Background sync', event.tag); if (event.tag === 'background-sync') { event.waitUntil(doBackgroundSync()); } if (event.tag === 'cart-sync') { event.waitUntil(syncCartData()); } }); // Background Sync Implementation async function doBackgroundSync() { try { // Synchronisiere Offline-Daten const offlineData = await getOfflineData(); for (const data of offlineData) { await syncToServer(data); } console.log('Service Worker: Background sync completed'); } catch (error) { console.error('Service Worker: Background sync failed', error); } } // Warenkorb-Synchronisation async function syncCartData() { try { const cartData = await getCartData(); if (cartData && cartData.items.length > 0) { await syncCartToServer(cartData); } console.log('Service Worker: Cart sync completed'); } catch (error) { console.error('Service Worker: Cart sync failed', error); } } // Push-Benachrichtigungen self.addEventListener('push', event => { console.log('Service Worker: Push notification received'); const options = { body: event.data ? event.data.text() : 'Neue Nachricht verfügbar', icon: '/img/icons/icon-192x192.png', badge: '/img/icons/badge-72x72.png', vibrate: [100, 50, 100], data: { dateOfArrival: Date.now(), primaryKey: 1 }, actions: [ { action: 'explore', title: 'Anzeigen', icon: '/img/icons/action-explore.png' }, { action: 'close', title: 'Schließen', icon: '/img/icons/action-close.png' } ] }; event.waitUntil( self.registration.showNotification('Webshop System', options) ); }); // Notification Click Handler self.addEventListener('notificationclick', event => { console.log('Service Worker: Notification clicked', event.action); event.notification.close(); if (event.action === 'explore') { event.waitUntil( clients.openWindow('/') ); } else if (event.action === 'close') { // Notification schließen return; } else { // Standard-Aktion: App öffnen event.waitUntil( clients.openWindow('/') ); } }); // Message Handler für Client-Kommunikation self.addEventListener('message', event => { console.log('Service Worker: Message received', event.data); if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } if (event.data && event.data.type === 'CACHE_URLS') { event.waitUntil( caches.open(STATIC_CACHE) .then(cache => cache.addAll(event.data.urls)) ); } if (event.data && event.data.type === 'GET_CACHE_SIZE') { event.waitUntil( caches.keys() .then(cacheNames => { return Promise.all( cacheNames.map(name => caches.open(name).then(cache => cache.keys())) ); }) .then(requests => { const totalSize = requests.reduce((size, requests) => size + requests.length, 0); event.ports[0].postMessage({ size: totalSize }); }) ); } }); // Hilfsfunktionen für Offline-Daten async function getOfflineData() { // Implementierung für Offline-Daten aus IndexedDB return []; } async function syncToServer(data) { // Implementierung für Server-Synchronisation console.log('Syncing data to server:', data); } async function getCartData() { // Implementierung für Warenkorb-Daten return null; } async function syncCartToServer(cartData) { // Implementierung für Warenkorb-Synchronisation console.log('Syncing cart to server:', cartData); } // Cache-Bereinigung async function cleanOldCaches() { const cacheNames = await caches.keys(); const currentCaches = [STATIC_CACHE, DYNAMIC_CACHE]; for (const cacheName of cacheNames) { if (!currentCaches.includes(cacheName)) { await caches.delete(cacheName); console.log('Service Worker: Deleted old cache', cacheName); } } } // Periodische Cache-Bereinigung setInterval(cleanOldCaches, 24 * 60 * 60 * 1000); // Täglich