Service Worker di TypeScript

Service Worker di TypeScript

Artikel ini menjelaskan tentang Service Worker di TypeScript.

Kami akan menjelaskan Service Worker di TypeScript, termasuk contoh praktis.

YouTube Video

Service Worker di TypeScript

Service Worker adalah 'proxy permintaan' yang berada di antara browser dan jaringan. Ini memungkinkan intersepsi fetch, kontrol cache, dukungan offline, dan pemrosesan di latar belakang (sinkronisasi dan push). Menggunakan TypeScript memberikan keamanan tipe dan meningkatkan kemudahan pemeliharaan.

Menyiapkan TypeScript

tsconfig.json (Aktifkan tipe WebWorker)

Mari kita lihat contoh mengaktifkan tipe WebWorker di tsconfig.json.

 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}
  • Dengan menambahkan WebWorker ke dalam array lib, Anda dapat menggunakan tipe seperti ServiceWorkerGlobalScope.
  • DOM dan WebWorker memiliki tipe yang berbeda, sehingga merupakan praktik umum untuk memisahkan pengaturan tsconfig.json untuk browser (aplikasi utama) dan Service Worker.
  • File Service Worker pada akhirnya akan dioutput ke jalur yang sesuai dengan cakupan (biasanya root situs /sw.js).
  • Karena alasan keamanan, Service Worker hanya berjalan melalui HTTPS (atau di localhost).

Kode Registrasi di Sisi Browser

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    );
  • Proses ini mendaftarkan sebuah Service Worker. scope mengacu pada rentang jalur yang dapat dikendalikan oleh Service Worker. Sebagai contoh, jika Anda menempatkan /sw.js langsung di bawah root dan menetapkan scope ke direktori root (/), Anda dapat mengendalikan semua sumber daya di seluruh situs. Sebaliknya, jika Anda menentukan direktori tertentu seperti /app/, hanya konten di bawah direktori tersebut yang akan dikendalikan.
1    // If there's a waiting worker, notify the user.
2    if (registration.waiting) {
3      promptUserToUpdate(registration);
4    }
  • waiting menunjukkan status di mana sebuah Service Worker baru telah terpasang dan sedang menunggu untuk diaktifkan. Pada tahap ini, halaman yang ada masih dikendalikan oleh Service Worker lama, jadi biasanya pengguna akan diminta konfirmasi, dan setelah mendapat persetujuan, panggil skipWaiting() untuk segera mengaktifkan Service Worker baru. Hal ini memungkinkan Anda menerapkan proses terbaru tanpa harus menunggu pemuatan ulang halaman berikutnya.
 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 dipicu saat instalasi Service Worker baru telah dimulai. Ketika peristiwa ini terjadi, worker baru akan diatur di registration.installing, sehingga dengan memantau statechange-nya, Anda dapat mendeteksi kapan instalasi telah selesai (installed). Selain itu, jika navigator.serviceWorker.controller ada, itu berarti Service Worker lama sudah mengontrol halaman, sehingga ini merupakan kesempatan untuk memberi tahu pengguna tentang keberadaan versi baru.
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}
  • Peristiwa controllerchange dipicu pada saat Service Worker baru mulai mengendalikan halaman saat ini. Memuat ulang pada saat ini akan segera menerapkan strategi cache dan pemrosesan yang baru. Namun, pemuatan ulang otomatis dapat menurunkan pengalaman pengguna, sehingga lebih baik memuat ulang setelah mendapat persetujuan pengguna.
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();
  • Dengan membuat Service Worker menerima postMessage({ type: 'SKIP_WAITING' }) dari klien dan kemudian memanggil self.skipWaiting(), Anda dapat meminta pembaruan.

Deklarasi Scope di sw.ts

Selanjutnya, mari kita lihat contoh Service Worker yang umum yang menerapkan caching app shell.

Saat menggunakan Service Worker di TypeScript, sangat berguna untuk memberikan tipe yang benar pada self.

1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
  • Di TypeScript, self secara default dianggap sebagai any, sehingga tanpa penambahan tipe, Anda tidak akan mendapatkan pelengkapan tipe atau pengecekan tipe untuk API khusus Service Worker seperti skipWaiting() atau clients.
  • Menentukan ServiceWorkerGlobalScope memungkinkan auto-completion, mencegah kesalahan penggunaan, dan memungkinkan pengembangan yang lebih aman terpisah dari skrip DOM biasa.

Service Worker Dasar (Install/Activate/Fetch)

Ini memperlihatkan manajemen versi cache sederhana, precaching saat install, menghapus cache lama saat activate, dan strategi cache saat fetch (cache-first untuk aset statis, network-first untuk API).

sw.ts (Pengaturan Minimal + Kerangka Cache)

 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});
  • Selama event install, sumber daya statis aplikasi (App Shell) akan di-cache terlebih dahulu. Dengan memanggil self.skipWaiting(), Service Worker baru akan segera diaktifkan sehingga cache terbaru langsung tersedia tanpa menunggu akses berikutnya.
 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});
  • Pada event activate, versi cache lama akan dihapus, dan Service Worker dijaga agar tetap terbaru. Selain itu, dengan memanggil self.clients.claim(), Service Worker baru dapat mengontrol semua klien tanpa menunggu halaman dimuat ulang.
 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});
  • Pada fetch, Anda dapat mencegat permintaan dan mengontrol respons. Anda dapat menerapkan strategi seperti cache-first atau network-first, yang berguna untuk dukungan offline dan performa.
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});
  • Jika SKIP_WAITING diterima, memanggil self.skipWaiting() memungkinkan Anda segera mengaktifkan Service Worker yang sedang menunggu. Sebagai hasilnya, versi baru akan diterapkan mulai permintaan berikutnya tanpa perlu memuat ulang halaman.

Gambaran Strategi Cache Praktis

cache-first

Cache-first memeriksa cache terlebih dahulu dan mengembalikan respons segera jika tersedia. Jika tidak, mengambil dari jaringan dan menyimpan hasilnya ke cache. Ini cocok untuk file statis.

 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}
  • Kode berikut menunjukkan implementasi cache-first. Jika terdapat cache, maka akan dikembalikan; jika tidak, mengambil dari jaringan lalu disimpan ke cache. Sangat cocok untuk sumber daya statis yang jarang berubah, seperti gambar atau CSS.

network-first

Network-first mencoba jaringan terlebih dahulu dan kembali ke cache jika gagal. Ini cocok untuk API di mana kesegaran data penting.

 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}
  • Kode berikut menunjukkan implementasi network-first. Jika ada respons dari jaringan, akan disimpan ke cache; jika gagal, akan mengembalikan versi cache. Sangat cocok untuk sumber data yang membutuhkan data segar, seperti berita atau respons API.

stale-while-revalidate

stale-while-revalidate akan mengembalikan cache terlebih dahulu dan sekaligus memperbaruinya dari jaringan di latar belakang. Ini menyeimbangkan kecepatan respons dan kesegaran data.

 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}
  • Kode ini mengembalikan cache langsung jika tersedia, sambil mengambil data baru dari jaringan di latar belakang untuk memperbarui cache. Ini memberikan respons cepat ke pengguna dan menggunakan konten terbaru untuk akses berikutnya, sehingga tepat untuk UI atau pengiriman data ringan.

Mengoptimalkan Alur Pembaruan (Notifikasi Pembaruan dan Reload Aman)

Pembaruan Service Worker tidak langsung diterapkan; versi baru akan tetap menunggu sampai semua tab yang ada ditutup.

Di sini, kami menerapkan sistem untuk memberi tahu klien saat versi baru siap dan memuat ulang halaman dengan aman berdasarkan tindakan pengguna.

Beritahu klien dari sisi Service Worker ketika versi baru sudah siap.

 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});
  • Pada kode ini, notifyClientsUpdated dipanggil di akhir event activate untuk memberi tahu semua klien terhubung bahwa versi baru sudah siap. clients.claim() adalah metode yang segera membawa halaman (klien) yang sedang terbuka di bawah kendali Service Worker yang baru saja diaktifkan. Biasanya, sebuah Service Worker mulai mengendalikan halaman hanya pada pemuatan berikutnya, tetapi dengan menggunakan clients.claim(), Anda dapat segera mengendalikan halaman tanpa pemuatan ulang.

Tampilkan UI pembaruan di klien, dan reload sesuai tindakan pengguna

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});
  • Klien menerima SW_UPDATED melalui event message dan menampilkan notifikasi pembaruan di UI. Ketika pengguna memilih untuk memuat ulang, window.location.reload() dijalankan sehingga HTML, CSS, dan sumber daya lainnya di halaman diperbarui ke versi terbaru. Hal ini memastikan bahwa cache dan kontrol oleh Service Worker yang beralih dengan clients.claim() tercermin di seluruh halaman.

Offline Fallback

Siapkan /offline.html untuk navigasi penting, dan sediakan UI minimal yang tetap bermakna meski tanpa gambar atau font. Jika panggilan API gagal, tampilkan status cache terakhir jika memungkinkan dan coba lakukan refetch di latar belakang untuk meningkatkan UX.

Contoh Implementasi

 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});
  • Lakukan precache pada /offline.html saat event install agar Anda bisa menampilkan halaman minimal saat jaringan tidak tersedia.
  • Pada event fetch, Anda dapat memonitor permintaan navigasi dengan request.mode === 'navigate' dan secara khusus menargetkan transisi halaman.
  • Gunakan fallback ke /offline.html saat jaringan gagal, memastikan halaman tampil meskipun offline.

Komunikasi pesan antara klien dan Service Worker

Karena Service Worker beroperasi secara independen dari siklus hidup halaman, pendistribusian pesan dua arah sangat penting untuk memberi tahu status dan mengeksekusi perintah. Menentukan tipe untuk pesan membantu mencegah pengiriman pesan yang salah, memungkinkan pelengkapan kode, dan membuat implementasi Anda lebih kuat.

Contoh Kode

  1. Definisi Tipe Pesan
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 adalah tipe pesan yang dikirim dari Service Worker ke klien.
  • ClientToSw adalah tipe pesan yang dikirim dari klien ke Service Worker.
  • Ini memungkinkan Anda memperjelas jenis-jenis peristiwa yang dapat dipertukarkan melalui komunikasi dua arah.
  1. Pemrosesan di sisi Service Worker
 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 menerima pesan dari klien dan memprosesnya berdasarkan tipe.
  • Untuk CLEAR_CACHE, cache akan dihapus lalu semua klien diberi tahu dengan CACHE_CLEARED.
  • Untuk PING, membalas ke klien asal dengan pesan PING yang berisi stempel waktu.
  1. Memberi Tahu Semua Klien dari Service Worker
1async function broadcast(msg: SwToClient) {
2  const clients = await self.clients.matchAll({ includeUncontrolled: true });
3  for (const c of clients) c.postMessage(msg);
4}
  • Gunakan clients.matchAll untuk mengambil semua tab jendela.
  • Dengan mengirim postMessage ke masing-masing, Anda bisa menyebarkan pesan ke semua.
  • Ini bisa digunakan untuk notifikasi pembaruan (seperti SW_UPDATED) dan notifikasi kesalahan.
  1. Pemrosesan di Sisi Klien
1navigator.serviceWorker.controller?.postMessage({
2  type: 'PING',
3  ts: Date.now()
4} as ClientToSw);
  • Dengan mengirimkan PING dari klien dan menerima respons dari Service Worker, Anda dapat memverifikasi bahwa komunikasi dua arah berjalan dengan baik. Ini memudahkan pengujian status koneksi dan penanganan pesan.
 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 の更新やリロード、通知表示などを行います。^}

Keuntungan Pesan Bertipe

  • Penggunaan pesan bertipe membuat pesan yang bisa dikirim dan diterima menjadi jelas, serta auto-completion dan pemeriksaan tipe meningkatkan keamanan.
  • postMessage memungkinkan komunikasi satu-ke-satu dan broadcast memungkinkan komunikasi satu-ke-banyak.
  • Anda dapat dengan mudah mengimplementasikan fitur penting seperti notifikasi pembaruan (SW_UPDATED), manajemen cache (CACHE_CLEARED), dan health check (PING).

Ringkasan

  • Menggunakan TypeScript menambah keamanan tipe pada pemanggilan API dan pesan Service Worker, sehingga meningkatkan efisiensi pengembangan dan kemudahan pemeliharaan.
  • Memahami event siklus hidup install, activate, dan fetch, serta memilih strategi cache yang tepat (seperti cache-first atau network-first) untuk tiap situasi akan menghasilkan pengalaman pengguna yang lebih baik.
  • Untuk operasi, memahami manajemen versi cache dan alur pembaruan (updatefound, waiting, SKIP_WAITING, dll.) sangat penting.
  • Dengan mengadopsi pesan bertipe untuk komunikasi antara klien dan Service Worker, Anda dapat mencegah implementasi yang salah dan membangun sistem yang mudah diperluas serta dipelihara dalam jangka panjang.

Anda dapat mengikuti artikel di atas menggunakan Visual Studio Code di saluran YouTube kami. Silakan periksa juga saluran YouTube kami.

YouTube Video