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 arraylib
, Anda dapat menggunakan tipe sepertiServiceWorkerGlobalScope
. DOM
danWebWorker
memiliki tipe yang berbeda, sehingga merupakan praktik umum untuk memisahkan pengaturantsconfig.json
untuk browser (aplikasi utama) danService 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 olehService Worker
. Sebagai contoh, jika Anda menempatkan/sw.js
langsung di bawah root dan menetapkanscope
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 olehService Worker
lama, jadi biasanya pengguna akan diminta konfirmasi, dan setelah mendapat persetujuan, panggilskipWaiting()
untuk segera mengaktifkanService 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 diregistration.installing
, sehingga dengan memantaustatechange
-nya, Anda dapat mendeteksi kapan instalasi telah selesai (installed
). Selain itu, jikanavigator.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
menerimapostMessage({ type: 'SKIP_WAITING' })
dari klien dan kemudian memanggilself.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 sebagaiany
, sehingga tanpa penambahan tipe, Anda tidak akan mendapatkan pelengkapan tipe atau pengecekan tipe untuk API khusus Service Worker sepertiskipWaiting()
atauclients
. - 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 memanggilself.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, danService Worker
dijaga agar tetap terbaru. Selain itu, dengan memanggilself.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, memanggilself.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 eventactivate
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, sebuahService Worker
mulai mengendalikan halaman hanya pada pemuatan berikutnya, tetapi dengan menggunakanclients.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 eventmessage
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 olehService Worker
yang beralih denganclients.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 eventinstall
agar Anda bisa menampilkan halaman minimal saat jaringan tidak tersedia. - Pada event
fetch
, Anda dapat memonitor permintaan navigasi denganrequest.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
- 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.
- 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 denganCACHE_CLEARED
. - Untuk
PING
, membalas ke klien asal dengan pesanPING
yang berisi stempel waktu.
- 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.
- 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 dariService 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 danbroadcast
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
, danfetch
, 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.