TypeScript'te Service Worker

TypeScript'te Service Worker

Bu makale, TypeScript'te Service Worker’ları açıklar.

TypeScript'te Service Worker’ları, pratik örneklerle birlikte açıklayacağız.

YouTube Video

TypeScript'te Service Worker

Bir Service Worker, tarayıcı ile ağ arasında yer alan bir 'istek vekili'dir. Fetch yakalama, önbellek kontrolü, çevrimdışı destek ve arka plan işlemleri (senkronizasyon ve push) sağlar. TypeScript kullanmak, tip güvenliği sağlar ve bakım kolaylığını artırır.

TypeScript Kurulumu

tsconfig.json (WebWorker türlerini etkinleştir)

tsconfig.json dosyasında WebWorker türünü etkinleştirme örneğine bakalım.

 1{
 2  "compilerOptions": {
 3    "target": "ES2020",
 4    "module": "ES2020",
 5    "lib": ["ES2020", "WebWorker"],
 6    "moduleResolution": "Bundler",
 7    "strict": true,
 8    "noEmitOnError": true,
 9    "outDir": "out",
10    "skipLibCheck": true
11  },
12  "include": ["sw.ts"]
13}
  • lib dizisine WebWorker ekleyerek ServiceWorkerGlobalScope gibi türleri kullanabilirsiniz.
  • DOM ve WebWorker farklı türlere sahiptir, bu nedenle tarayıcı (ana uygulama) ve Service Worker için tsconfig.json ayarlarını ayırmak yaygın bir uygulamadır.
  • Service Worker dosyaları nihayetinde scope ile eşleşen bir yola (genellikle site kök dizini /sw.js) çıktı olarak verilir.
  • Güvenlik nedeniyle, Service Worker’lar yalnızca HTTPS (veya localhost) üzerinden çalışır.

Tarayıcı Tarafında Kayıt Kodu

register-sw.ts

1// register-sw.ts
2async function registerServiceWorker() {
3  if (!('serviceWorker' in navigator)) return;
4
5  try {
6    const registration = await navigator.serviceWorker.register(
7      '/sw.js', { scope: '/' }
8    );
  • Bu işlem bir Service Worker kaydeder. scope, Service Workerın denetleyebileceği yolların aralığını ifade eder. Örneğin, /sw.js dosyasını doğrudan kök dizine yerleştirir ve scopeu kök dizin (/) olarak ayarlarsanız, sitenin tüm kaynaklarını kontrol edebilirsiniz. Öte yandan, /app/ gibi belirli bir dizin belirtirseniz, yalnızca o dizin altındaki içerikler kontrol altında olacaktır.
1    // If there's a waiting worker, notify the user.
2    if (registration.waiting) {
3      promptUserToUpdate(registration);
4    }
  • waiting, yeni bir Service Worker'ın kurulduğu ve etkinleştirilmek için beklediği durumunu gösterir. Bu aşamada, mevcut sayfalar hâlâ eski Service Worker tarafından kontrol edilir, bu nedenle genellikle kullanıcıdan onay istenir ve onay alındıktan sonra yeni Service Workerı hemen etkinleştirmek için skipWaiting() çağrılır. Bu, bir sonraki sayfa yenilemesini beklemeden en son işlemleri yansıtmanıza olanak tanır.
 1    // When a new SW is installing, monitor its state changes
 2    registration.addEventListener('updatefound', () => {
 3      const newWorker = registration.installing;
 4      if (!newWorker) return;
 5      newWorker.addEventListener('statechange', () => {
 6        if (newWorker.state === 'installed' &&
 7            navigator.serviceWorker.controller) {
 8          // New content available, prompt the user
 9          promptUserToUpdate(registration);
10        }
11      });
12    });
  • updatefound, yeni bir Service Worker yüklemesi başladığında tetiklenir. Bu olay meydana geldiğinde, registration.installing içine yeni bir worker atanır, bu nedenle onun statechange değişikliğini izleyerek yüklemenin ne zaman tamamlandığını (installed) tespit edebilirsiniz. Ayrıca, navigator.serviceWorker.controller mevcutsa, bu sayfanın zaten eski bir Service Worker tarafından kontrol edildiği anlamına gelir, bu nedenle kullanıcıya yeni bir sürümün mevcut olduğu bildirilmelidir.
1    // When the active worker changes (e.g., after skipWaiting), reload if desired
2    navigator.serviceWorker.addEventListener('controllerchange', () => {
3      // Optionally reload to let the new SW take over
4      window.location.reload();
5    });
6  } catch (err) {
7    console.error('Service Worker registration failed: ', err);
8  }
9}
  • controllerchange olayı, yeni Service Worker mevcut sayfayı kontrol etmeye başladığı anda tetiklenir. Bu noktada sayfanın yenilenmesi, yeni önbellek stratejilerini ve işlemleri hemen uygular. Ancak, otomatik yenileme kullanıcı deneyimini bozabilir; bu nedenle kullanıcının onayını aldıktan sonra sayfanın yenilenmesi tercih edilir.
1function promptUserToUpdate(reg: ServiceWorkerRegistration) {
2  // Show UI to user. If user accepts:
3  if (reg.waiting) {
4    reg.waiting.postMessage({ type: 'SKIP_WAITING' });
5  }
6}
7
8registerServiceWorker();
  • Service Workerın istemciden postMessage({ type: 'SKIP_WAITING' }) mesajı alması ve ardından self.skipWaiting() çağrısıyla güncellemeyi hemen başlatmasını sağlayabilirsiniz.

sw.ts'de Scope Bildirimi

Şimdi, uygulama kabuğu önbelleklemesi uygulayan tipik bir Service Worker örneğine bakalım.

TypeScript'te Service Worker kullanırken, self'e doğru türü atamak faydalıdır.

1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
  • TypeScript'te self varsayılan olarak any olarak kabul edilir, bu yüzden ek tip tanımlamaları olmadan, skipWaiting() veya clients gibi Service Worker'a özgü API'ler için otomatik tamamlama veya tür kontrolü alamazsınız.
  • ServiceWorkerGlobalScope belirterek otomatik tamamlama elde edersiniz, hatalı kullanım engellenir ve normal DOM betiklerinden ayrılmış daha güvenli bir geliştirme yapabilirsiniz.

Temel Service Worker (Kurulum/Aktif Etme/Getirme)

Bu örnek, basit önbellek sürüm yönetimini, kurulumda önbelleğe alma, etkinleştirmede eski önbellekleri silme ve getirme sırasında önbellek stratejilerini (statik dosyalar için önbellek-öncelikli, API'ler için ağ-öncelikli) gösterir.

sw.ts (Minimum Kurulum + Önbellek İskeleti)

 1const CACHE_NAME = 'app-shell-v1';
 2const STATIC_ASSETS = [
 3  '/',
 4  '/index.html',
 5  '/styles.css',
 6  '/main.js',
 7  '/fallback.png'
 8];
 9
10self.addEventListener('install', (event: ExtendableEvent) => {
11  // Pre-cache application shell
12  event.waitUntil(
13    caches.open(CACHE_NAME)
14      .then(cache => cache.addAll(STATIC_ASSETS))
15      // Activate immediately (optional: coordinate with client)
16      .then(() => self.skipWaiting())
17  );
18});
  • install olayı sırasında, uygulamanın statik kaynakları (App Shell) önbelleğe alınır. self.skipWaiting() fonksiyonu çağrılarak, yeni Service Worker hemen etkinleştirilir ve bir sonraki erişimi beklemeden en güncel önbellek kullanılabilir hale gelir.
 1self.addEventListener('activate', (event: ExtendableEvent) => {
 2  // Clean up old caches and take control of clients immediately
 3  event.waitUntil(
 4    caches.keys().then(keys =>
 5      Promise.all(keys
 6        .filter(key => key !== CACHE_NAME)
 7        .map(key => caches.delete(key)))
 8    ).then(() => self.clients.claim())
 9  );
10});
  • activate olayı sırasında, eski önbellek sürümleri silinir ve Service Worker güncel tutulur. Ayrıca, self.clients.claim() fonksiyonu çağrılarak, yeni Service Worker sayfa yeniden yüklenmeden tüm istemcileri kontrol edebilir.
 1self.addEventListener('fetch', (event: FetchEvent) => {
 2  const request = event.request;
 3  const url = new URL(request.url);
 4
 5  // Navigation requests (SPA) -> network-first with fallback to cached index.html
 6  if (request.mode === 'navigate') {
 7    event.respondWith(
 8      fetch(request).catch(() => caches.match('/index.html') as Promise<Response>)
 9    );
10    return;
11  }
12
13  // Simple API routing: network-first for /api/
14  if (url.pathname.startsWith('/api/')) {
15    event.respondWith(networkFirst(request));
16    return;
17  }
18
19  // Static assets: cache-first
20  event.respondWith(cacheFirst(request));
21});
  • fetch aşamasında, istekleri yakalayıp cevabı kontrol edebilirsiniz. Çevrimdışı destek ve performans için faydalı olan önbellek-öncelikli veya ağ-öncelikli gibi stratejiler uygulayabilirsiniz.
1self.addEventListener('message', (event: ExtendableMessageEvent) => {
2  const data = (event as any).data;
3  if (!data) return;
4
5  if (data.type === 'SKIP_WAITING') {
6    // Force the waiting service worker to become active
7    self.skipWaiting();
8  }
9});
  • SKIP_WAITING mesajı alındığında, self.skipWaiting() çağrısı ile beklemedeki Service Worker'ı anında etkinleştirebilirsiniz. Sonuç olarak, sayfayı yeniden yüklemeye gerek olmadan yeni sürüm bir sonraki istekten itibaren uygulanacaktır.

Pratik Önbellek Stratejisi Özeti

cache-first

Önce önbellek (Cache-first) önce önbelleği kontrol eder ve varsa hemen yanıt döndürür. Yoksa, ağdan getirir ve sonucu önbelleğe kaydeder. Bu, statik dosyalar için uygundur.

 1async function cacheFirst(request: Request): Promise<Response> {
 2  const cache = await caches.open(CACHE_NAME);
 3  const cached = await cache.match(request);
 4  if (cached) {
 5    return cached;
 6  }
 7  const response = await fetch(request);
 8  if (response && response.ok) {
 9    cache.put(request, response.clone());
10  }
11  return response;
12}
  • Bu kod, önbellek-öncelikli bir uygulamayı gösterir. Önbellekte varsa döndürür; yoksa ağdan getirip önbelleğe kaydeder. Nadiren değişen resim veya CSS gibi statik kaynaklar için uygundur.

network-first

Önce ağ (Network-first) önce ağı dener ve başarısız olursa önbelleğe döner. Bu, tazelik gerektiren API’ler için uygundur.

 1async function networkFirst(request: Request): Promise<Response> {
 2  const cache = await caches.open(CACHE_NAME);
 3  try {
 4    const response = await fetch(request);
 5    if (response && response.ok) {
 6      cache.put(request, response.clone());
 7    }
 8    return response;
 9  } catch (err) {
10    const cached = await cache.match(request);
11    if (cached) return cached;
12    return new Response(JSON.stringify({ error: 'offline' }), {
13      status: 503,
14      headers: { 'Content-Type': 'application/json' }
15    });
16  }
17}
  • Bu kod, ağ-öncelikli bir uygulamayı gösterir. Ağdan yanıt alınırsa önbelleğe kaydeder; başarısız olursa önbellektekini döndürür. Haberler veya API yanıtları gibi güncel veri gerektiren kaynaklar için uygundur.

stale-while-revalidate

stale-while-revalidate önce önbelleği döndürür ve aynı anda arka planda ağı kullanarak önbelleği günceller. Bu, yanıt hızı ile tazeliği dengeler.

 1async function staleWhileRevalidate(request: Request, cacheName = CACHE_NAME): Promise<Response> {
 2  const cache = await caches.open(cacheName);
 3  const cachedResponse = await cache.match(request);
 4  const networkFetch = fetch(request).then(networkResponse => {
 5    if (networkResponse && networkResponse.ok) {
 6      cache.put(request, networkResponse.clone());
 7    }
 8    return networkResponse;
 9  }).catch(() => undefined);
10
11  // Return cached immediately if exists, otherwise wait network
12  return cachedResponse || (await networkFetch) || new Response('offline', { status: 503 });
13}
  • Bu kod, önbellekte varsa hemen döndürürken arka planda ağdan yeni veriyi alır ve önbelleği günceller. Kullanıcıya hızlı yanıt sağlar ve bir sonraki erişimde güncellenmiş içeriği kullanarak UI veya hafif veri aktarımı için uygun hale getirir.

Güncelleme Akışını Optimize Etme (Güncelleme Bildirimi ve Güvenli Yeniden Yükleme)

Service Worker güncellemeleri hemen devreye girmez; yeni sürüm, mevcut sekmeler kapatılıncaya kadar beklemede kalır.

Burada, yeni sürüm hazır olduğunda istemciye bildiren ve kullanıcı eylemine göre sayfayı güvenle yeniden yükleyen bir sistem uyguluyoruz.

Yeni sürüm hazır olduğunda istemciyi Service Worker tarafından bilgilendirin.

 1// In sw.ts: after 'activate' or when new version is ready, broadcast a message
 2async function notifyClientsUpdated() {
 3  const all = await self.clients.matchAll({ type: 'window' });
 4  for (const client of all) {
 5    client.postMessage({ type: 'SW_UPDATED' });
 6  }
 7}
 8
 9// e.g., call this at the end of 'activate'
10self.addEventListener('activate', (event) => {
11  event.waitUntil((async () => {
12    if ('navigationPreload' in self.registration) {
13      await self.registration.navigationPreload.enable();
14    }
15    // cache cleanup
16    const cacheNames = await caches.keys();
17    await Promise.all(
18      cacheNames.map((name) => {
19        if (name !== CACHE_NAME) {
20          return caches.delete(name);
21        }
22      })
23    );
24
25    await self.clients.claim();
26    await notifyClientsUpdated();
27  })());
28});
  • Bu örnekte, activate olayının sonunda notifyClientsUpdated çağrılarak tüm bağlı istemcilere yeni sürümün hazır olduğu bildirilir. clients.claim(), şu anda açık olan sayfaları (istemcileri) yeni etkinleştirilen Service Worker'ın kontrolüne anında almayı sağlayan bir yöntemdir. Normalde, bir Service Worker sayfayı yalnızca bir sonraki yükleme sırasında kontrol etmeye başlar, ancak clients.claim() kullanarak sayfayı yeniden yüklemeden hemen kontrol altına alabilirsiniz.

İstemcide güncelleme arayüzünü gösterin ve kullanıcı işlemiyle yeniden yükleyin

1// in app startup
2navigator.serviceWorker.addEventListener('message', (e) => {
3  if (e.data?.type === 'SW_UPDATED') {
4    // Show a non-intrusive toast or banner: "New version available"
5    // When user clicks "Reload", call:
6    window.location.reload();
7  }
8});
  • İstemci, message olayıyla SW_UPDATED alır ve kullanıcı arayüzünde bir güncelleme bildirimi gösterir. Kullanıcı yeniden yüklemeyi seçtiğinde, window.location.reload() çalıştırılır ve sayfadaki eski HTML, CSS ve diğer kaynaklar en son sürüme güncellenir. Bu, önbelleğin ve clients.claim() ile devreye alınan Service Worker kontrolünün tüm sayfada geçerli olmasını sağlar.

Çevrimdışı Yedek Sayfa

Kritik gezinme için /offline.html hazırlayın ve resim ya da font olmadan da anlamı ileten minimal bir arayüz sunun. Bir API çağrısı başarısız olursa, mümkünse son önbellek durumunu gösterin ve arka planda tekrar getirmeye çalışarak kullanıcı deneyimini artırın.

Uygulama Örneği

 1// sw.ts
 2const CACHE_NAME = 'app-cache-v1';
 3
 4// Cache offline.html during install
 5self.addEventListener('install', (event) => {
 6  event.waitUntil((async () => {
 7    const cache = await caches.open(CACHE_NAME);
 8    await cache.addAll(['/offline.html']);
 9  })());
10});
11
12// Handle fetch requests
13self.addEventListener('fetch', (event) => {
14  const request = event.request;
15
16  // Navigation requests (e.g., page transitions)
17  if (request.mode === 'navigate') {
18    event.respondWith((async () => {
19      try {
20        // Try to fetch from the network as usual
21        return await fetch(request);
22      } catch (err) {
23        // On failure, return offline fallback page
24        const cache = await caches.open(CACHE_NAME);
25        return await cache.match('/offline.html') as Response;
26      }
27    })());
28  }
29});
  • install aşamasında /offline.html önbelleğe alın, böylece ağ yoksa en azından minimal bir sayfa döndürebilirsiniz.
  • fetch olayında, request.mode === 'navigate' ile yönlendirme isteklerini izleyebilir ve özellikle sayfa geçişlerini hedefleyebilirsiniz.
  • Ağ başarısız olduğunda /offline.html’e yönlendirin, böylece çevrimdışıyken de gösterilir.

İstemci ile Service Worker arasındaki mesajlaşma

Service Worker sayfa yaşam döngüsünden bağımsız çalıştığı için, durumları bildirmek ve komutları yürütmek için çift yönlü mesajlaşma önemlidir. Mesajlar için tip belirtmek, yanlış mesaj göndermeyi önler, kod tamamlama sağlar ve uygulamanızın daha sağlam olmasına yardımcı olur.

Kod Örneği

  1. Mesaj Türlerinin Tanımı
1type SwToClient =
2  | { type: 'SW_READY' }
3  | { type: 'SW_UPDATED' }
4  | { type: 'CACHE_CLEARED' }
5  | { type: 'PING'; ts: number };
6
7type ClientToSw =
8  | { type: 'CLEAR_CACHE' }
9  | { type: 'PING'; ts: number };
  • SwToClient, Service Worker'dan istemciye gönderilen mesajın türüdür.
  • ClientToSw, istemciden Service Worker'a gönderilen mesajın türüdür.
  • Bu, çift yönlü iletişim yoluyla hangi türde olayların değiş tokuş edilebileceğini netleştirmenizi sağlar.
  1. Service Worker Tarafında İşleme
 1self.addEventListener('message', (event) => {
 2  const data = event.data as ClientToSw;
 3  if (data?.type === 'CLEAR_CACHE') {
 4    event.waitUntil((async () => {
 5      const keys = await caches.keys();
 6      await Promise.all(keys.map((k) => caches.delete(k)));
 7      await broadcast({ type: 'CACHE_CLEARED' });
 8    })());
 9  } else if (data?.type === 'PING') {
10    event.source?.postMessage({ type: 'PING', ts: data.ts } as SwToClient);
11  }
12});
  • Service Worker, istemciden mesajları alır ve türüne göre işlem dallandırır.
  • CLEAR_CACHE için, önbelleği siler ve ardından tüm istemcilere CACHE_CLEARED ile bildirir.
  • PING için, orijinal istemciye zaman damgası içeren bir PING mesajıyla yanıt verir.
  1. Tüm İstemcileri Service Worker'dan Bildirme
1async function broadcast(msg: SwToClient) {
2  const clients = await self.clients.matchAll({ includeUncontrolled: true });
3  for (const c of clients) c.postMessage(msg);
4}
  • Tüm pencere sekmelerini almak için clients.matchAll kullanın.
  • Her birine postMessage göndererek mesajı yayınlayabilirsiniz.
  • Bu, güncelleme bildirimleri (SW_UPDATED gibi) ve hata bildirimleri için kullanılabilir.
  1. İstemci Tarafında İşleme
1navigator.serviceWorker.controller?.postMessage({
2  type: 'PING',
3  ts: Date.now()
4} as ClientToSw);
  • İstemciden bir PING gönderip, Service Worker'dan yanıt alarak çift yönlü iletişimin düzgün çalıştığını doğrulayabilirsiniz. Bu yöntem, bağlantı durumlarını ve mesaj işleyişini test etmeyi kolaylaştırır.
 1navigator.serviceWorker.addEventListener('message', (e) => {
 2  const msg = e.data as SwToClient;
 3  switch (msg.type) {
 4    case 'SW_READY':
 5      console.log('Service Worker is ready');
 6      // Example: hide loading spinner or enable offline UI
 7      break;
 8    case 'SW_UPDATED':
 9      console.log('A new version of the Service Worker is available');
10      // Example: show update notification or reload prompt
11      const shouldReload = confirm('A new version is available. Reload now?');
12      if (shouldReload) {
13        window.location.reload();
14      }
15      break;
16    case 'CACHE_CLEARED':
17      console.log('Cache cleared');
18      // Example: show confirmation message to user
19      alert('Cache has been successfully cleared.');
20      break;
21    case 'PING':
22      console.log(`Received PING response, ts=${msg.ts}`);
23      break;
24  }
25});
  • {^ i18n_speak クライアント側では Service Worker から送信されるメッセージを受信し、種類に応じて処理を分岐します。SW_READY は初期化完了、SW_UPDATED は新バージョン検出、CACHE_CLEARED はキャッシュ削除完了、PING は通信確認を示します。各メッセージに応じて、UI の更新やリロード、通知表示などを行います。^}

Tipli Mesajlaşmanın Faydaları

  • Tipli mesajlar kullanmak, hangi mesajların gönderilip alınabileceğini netleştirir ve otomatik tamamlama ile tip kontrolü güvenliği artırır.
  • postMessage, birebir iletişim; broadcast, birden çoğa iletişim sağlar.
  • Güncelleme bildirimleri (SW_UPDATED), önbellek yönetimi (CACHE_CLEARED) ve sağlık denetimleri (PING) gibi temel özellikleri kolayca uygulayabilirsiniz.

Özet

  • TypeScript kullanmak, Service Worker API çağrılarına ve mesajlaşmaya tip güvenliği ekler; bu da geliştirme verimliliğini ve bakım kolaylığını büyük ölçüde artırır.
  • install, activate ve fetch yaşam döngüsü olaylarını anlamak ve her durum için doğru önbellekleme stratejisini (cache-first veya network-first gibi) seçmek, daha iyi bir kullanıcı deneyimi sağlar.
  • Operasyonlar için, önbellek sürüm yönetimini ve güncelleme akışlarını (updatefound, waiting, SKIP_WAITING vb.) anlamak çok önemlidir.
  • İstemci-Service Worker iletişimi için tipli mesajlaşma kullanarak yanlış uygulamaları önleyebilir ve uzun vadede kolayca genişletilip bakım yapılabilecek bir sistem oluşturabilirsiniz.

Yukarıdaki makaleyi, YouTube kanalımızda Visual Studio Code'u kullanarak takip edebilirsiniz. Lütfen YouTube kanalını da kontrol edin.

YouTube Video