İçeriğe geç
JavaScript

JavaScript Service Workers ve Offline Destek

Tarık Tunç
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ü

  1. Registration (Kayıt): Ana script, Service Worker'ı kaydeder
  2. Installation (Kurulum): Dosyalar cache'lenir
  3. Activation (Etkinleştirme): Eski cache temizlenir, kontrol alınır
  4. 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));
});
Bilgi: Her strateji farklı senaryolar için uygundur. Sık güncellenmeyen varlıklar için Cache First, güncel veri gerektiren API'ler için Network First, dengeli yaklaşım için Stale While Revalidate kullanın.

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)
    );
  }
});
Uyarı: Service Worker'lar yalnızca HTTPS üzerinde çalışır (localhost hariç). Push notification'lar için VAPID anahtarları gereklidir ve kullanıcıdan izin alınmalıdır.

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ı
});
İpucu: Service Worker güncellemelerinde kullanıcıya bildirim gösterin ve sayfayı yenilemelerini isteyin. 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.

Kaynaklar

İlgili Yazılar