JavaScript Service Workers ve Offline Destek

JavaScript Service Workers ve Offline Destek
Service Worker'lar, tarayıcı ile ağ arasına oturan programlanabilir proxy'lerdir. Ağ isteklerini yakalama, cache stratejileri uygulama ve çevrimdışı çalışma gibi yetenekleriyle Progressive Web App (PWA) mimarisinin temelini oluştururlar. Bu rehberde Service Worker yaşam döngüsünü, cache stratejilerini, push notification entegrasyonunu ve offline-first uygulama geliştirmeyi adım adım inceleyeceğiz.
Service Worker Yaşam Döngüsü
- Registration (Kayıt): Ana script, Service Worker'ı kaydeder
- Installation (Kurulum): Dosyalar cache'lenir
- Activation (Etkinleştirme): Eski cache temizlenir, kontrol alınır
- Fetch (Yakalama): Ağ istekleri yakalanır ve yanıtlanır
Service Worker Kaydı
// app.js (Ana script)
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/',
});
console.log('SW kayıtlı, scope:', registration.scope);
// Güncelleme kontrolü
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
console.log('Yeni SW bulundu, state:', newWorker.state);
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
// Kullanıcıya güncelleme bildirimi göster
showUpdateNotification();
}
});
});
} catch (error) {
console.error('SW kayıt hatası:', error);
}
});
}
Install ve Activate Olayları
// sw.js (Service Worker)
const CACHE_NAME = 'app-cache-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png',
'/offline.html',
];
// Install: Statik dosyaları cache'le
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('Statik dosyalar cache\'leniyor');
return cache.addAll(STATIC_ASSETS);
})
);
// Bekleme aşamasını atla
self.skipWaiting();
});
// Activate: Eski cache'leri temizle
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => {
console.log('Eski cache siliniyor:', name);
return caches.delete(name);
})
);
})
);
// Tüm açık sekmelerin kontrolünü al
self.clients.claim();
});
Cache Stratejileri
1. Cache First (Offline First)
// Önce cache'e bak, yoksa ağa git
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request).then((networkResponse) => {
// Yanıtı cache'e de ekle
const responseClone = networkResponse.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return networkResponse;
});
})
);
});
2. Network First
// Önce ağa git, başarısızsa cache'den sun
function networkFirst(request) {
return fetch(request)
.then((response) => {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseClone);
});
return response;
})
.catch(() => {
return caches.match(request);
});
}
3. Stale While Revalidate
// Cache'den hemen sun, arka planda güncelle
function staleWhileRevalidate(request) {
return caches.open(CACHE_NAME).then((cache) => {
return cache.match(request).then((cachedResponse) => {
const fetchPromise = fetch(request).then((networkResponse) => {
cache.put(request, networkResponse.clone());
return networkResponse;
});
return cachedResponse || fetchPromise;
});
});
}
Strateji Seçimi
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// API istekleri: Network First
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request));
return;
}
// Statik varlıklar: Cache First
if (request.destination === 'image' ||
request.destination === 'font' ||
request.destination === 'style') {
event.respondWith(cacheFirst(request));
return;
}
// HTML sayfalar: Stale While Revalidate
if (request.mode === 'navigate') {
event.respondWith(
staleWhileRevalidate(request).catch(() => {
return caches.match('/offline.html');
})
);
return;
}
// Varsayılan: Network First
event.respondWith(networkFirst(request));
});
Offline Sayfası
<!-- offline.html -->
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Çevrimdışı</title>
<style>
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: system-ui;
text-align: center;
background: #f5f5f5;
}
</style>
</head>
<body>
<div>
<h1>Çevrimdışısınız</h1>
<p>İnternet bağlantınızı kontrol edip tekrar deneyin.</p>
<button>Tekrar Dene</button>
</div>
</body>
</html>
Background Sync
Çevrimdışıyken yapılan işlemleri bağlantı geldiğinde otomatik senkronize edin:
// Ana script: Sync kaydı
async function submitForm(data) {
try {
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(data),
});
} catch (error) {
// Çevrimdışı: IndexedDB'ye kaydet ve sync planla
await saveToIndexedDB('pending-submissions', data);
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('submit-form');
}
}
// sw.js: Sync olayını işle
self.addEventListener('sync', (event) => {
if (event.tag === 'submit-form') {
event.waitUntil(
getFromIndexedDB('pending-submissions').then((items) => {
return Promise.all(
items.map((item) =>
fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(item),
}).then(() => removeFromIndexedDB('pending-submissions', item.id))
)
);
})
);
}
});
Push Notifications
// Ana script: Push aboneliği
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});
// Aboneliği sunucuya gönder
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: { 'Content-Type': 'application/json' },
});
}
// sw.js: Push olayını işle
self.addEventListener('push', (event) => {
const data = event.data?.json() || {};
event.waitUntil(
self.registration.showNotification(data.title || 'Bildirim', {
body: data.body || '',
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
data: { url: data.url || '/' },
actions: [
{ action: 'open', title: 'Aç' },
{ action: 'dismiss', title: 'Kapat' },
],
})
);
});
// Bildirime tıklama
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'open' || !event.action) {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});
Next.js ile Service Worker
// next.config.js (next-pwa ile)
// npm install @ducanh2912/next-pwa
const withPWA = require('@ducanh2912/next-pwa')({
dest: 'public',
disable: process.env.NODE_ENV === 'development',
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxEntries: 50, maxAgeSeconds: 300 },
},
},
],
});
module.exports = withPWA({
// next.config.js ayarları
});
skipWaiting() kullanmak beklenmedik davranışlara yol açabilir.
Sonuç
Service Worker'lar, web uygulamalarına çevrimdışı çalışma, arka plan senkronizasyon ve push notification yetenekleri kazandırır. Doğru cache stratejisi seçimi kritik önem taşır: statik varlıklar için Cache First, dinamik veriler için Network First, dengeli yaklaşım için Stale While Revalidate kullanın. Background Sync ile çevrimdışı işlemleri senkronize edin ve kullanıcılara her koşulda çalışan bir deneyim sunun.