Service Worker sa TypeScript

Service Worker sa TypeScript

Ipinaliwanag ng artikulong ito ang Service Workers sa TypeScript.

Ipapaliwanag namin ang Service Workers sa TypeScript, kasama ang mga praktikal na halimbawa.

YouTube Video

Service Worker sa TypeScript

Ang Service Worker ay isang "request proxy" na namamagitan sa pagitan ng browser at ng network. Ipinapahintulot nito ang fetch interception, control sa cache, suporta kahit offline, at background processing (sync at push). Ang paggamit ng TypeScript ay nagbibigay ng type safety at nagpapataas ng maintainability.

Pagse-setup ng TypeScript

tsconfig.json (I-enable ang WebWorker types)

Tingnan natin ang isang halimbawa ng pag-enable ng WebWorker type sa 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}
  • Sa pagdagdag ng WebWorker sa lib array, maaari mong gamitin ang mga types tulad ng ServiceWorkerGlobalScope.
  • Magkaiba ang mga uri ng \DOM` at `WebWorker`, kaya karaniwan nang ihiwalay ang mga setting ng `tsconfig.json` para sa browser (main app) at sa `Service Worker`.
  • Service Worker na mga file ay inilalabas sa landas na tumutugma sa scope (karaniwan ay ang root ng site /sw.js).
  • Dahil sa seguridad, ang Service Workers ay gumagana lamang sa HTTPS (o sa localhost).

Code ng Pagrehistro sa Panig ng 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    );
  • Ang prosesong ito ay nagrerehistro ng isang Service Worker. Ang scope ay tumutukoy sa saklaw ng mga landas na maaaring kontrolin ng Service Worker. Halimbawa, kung ilalagay mo ang /sw.js sa ilalim ng root at itatakda ang scope sa root directory (/), makokontrol mo ang lahat ng resources ng buong site. Sa kabilang banda, kung itutukoy mo ang isang partikular na directory gaya ng /app/, tanging ang mga nilalaman lamang sa ilalim ng directory na iyon ang makokontrol.
1    // If there's a waiting worker, notify the user.
2    if (registration.waiting) {
3      promptUserToUpdate(registration);
4    }
  • Ang waiting ay tumutukoy sa estado kung saan ang bagong Service Worker ay na-install na at naghihintay na ma-activate. Sa yugtong ito, ang mga kasalukuyang pahina ay kinokontrol pa rin ng lumang Service Worker, kaya karaniwan ang humiling ng kumpirmasyon mula sa user, at pagkatapos ng pag-apruba, tawagin ang skipWaiting() upang agad na ma-activate ang bagong Service Worker. Dahil dito, maipapakita ang pinakabagong mga proseso nang hindi na kailangang maghintay ng muling pag-reload ng pahina.
 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    });
  • Ang updatefound ay na-trigger kapag nagsimula na ang pag-install ng isang bagong Service Worker. Kapag nangyari ang event na ito, may bagong worker na nakatakda sa registration.installing, kaya't sa pagmamanman ng statechange nito, matutukoy mo kung kailan natapos ang pag-install (installed). Dagdag pa rito, kung umiiral ang `navigator.serviceWorker.controller`, nangangahulugan ito na may lumang Service Worker na kumokontrol sa pahina, kaya ito ay pagkakataon upang abisuhan ang user tungkol sa bagong bersyon.
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}
  • Ang controllerchange event ay nag-trigger kapag nagsimulang kontrolin ng bagong Service Worker ang kasalukuyang pahina. Kapag nireload sa puntong ito, agad na maa-apply ang mga bagong cache na estratehiya at proseso. Gayunpaman, ang awtomatikong pag-reload ay maaaring makasama sa karanasan ng user, kaya mas mainam na mag-reload lamang pagkatapos makuha ang pahintulot ng user.
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();
  • Sa pamamagitan ng pagpapadala ng postMessage({ type: 'SKIP_WAITING' }) mula sa client papunta sa Service Worker at pagtawag sa self.skipWaiting(), maaari mong simulan agad ang pag-update.

Pahayag ng Scope sa sw.ts

Susunod, tingnan natin ang isang karaniwang halimbawa ng Service Worker na may app shell caching.

Kapag gumagamit ng Service Workers sa TypeScript, mahalagang bigyan ng tamang type ang self.

1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
  • Sa TypeScript, itinuturing na `any` ang `self` bilang default, kaya kung walang karagdagang uri, hindi ka makakakuha ng type completion o type checking para sa mga partikular na API ng Service Worker gaya ng `skipWaiting()` o `clients`.
  • Kung iko-convert sa ServiceWorkerGlobalScope, magkakaroon ng auto-completion, iiwas sa maling paggamit, at magbibigay-daan sa mas ligtas na development na hiwalay sa regular na DOM scripts.

Pangunahing Service Worker (Install/Activate/Fetch)

Ipinapakita nito ang simpleng pamamahala ng cache version, pag-precash sa install, pagtanggal ng lumang cache kapag activate, at mga cache strategy sa fetch (cache-first para sa static assets, network-first para sa APIs).

sw.ts (Minimal na Setup + Balangkas ng 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});
  • Sa panahon ng `install` event, ang mga static resources (App Shell) ng app ay naka-pre-cache. Sa pagtawag ng `self.skipWaiting()`, agad na maa-activate ang bagong `Service Worker` at agad magagamit ang pinakabagong cache nang hindi na kailangang hintayin ang susunod na access.
 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});
  • Sa `activate` event, binubura ang mga lumang bersyon ng cache at napapanatiling napapanahon ang `Service Worker`. Dagdag pa, sa pagtawag ng `self.clients.claim()`, maaaring kontrolin ng bagong `Service Worker` ang lahat ng kliyente nang hindi hinihintay na ma-reload ang pahina.
 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});
  • Sa fetch, maaari mong salubungin at kontrolin ang tugon sa request. Maaaring magpatupad ng mga strategy tulad ng cache-first at network-first, na mahalaga para sa offline support at performance.
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});
  • Kung matatanggap ang SKIP_WAITING, ang pagtawag ng self.skipWaiting() ay nagbibigay-daan sa iyo na agad i-activate ang naghihintay na Service Worker. Bilang resulta, ang bagong bersyon ay maipapatupad na agad sa susunod na request nang hindi na kailangan pang i-reload ang pahina.

Pangkalahatang tanaw sa Mga Praktikal na Cache Strategy

cache-first

Cache-first ay unang tumitingin sa cache at agad na bumabalik ng sagot kung mayroon. Kung wala, kukuha ito mula sa network at icacache ang resulta. Ito ay akma para sa mga static na file.

 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}
  • Ipinapakita ng code na ito ang cache-first na implementasyon. Kung may cache, ito ay ipinapadala agad; kung wala, kukuha mula sa network at ise-save ito sa cache. Angkop ito para sa mga static na resource na bihirang magbago, tulad ng mga larawan o CSS.

network-first

Network-first ay unang sumusubok sa network at babalik sa cache kapag nabigo. Ito ay gamit para sa mga API na mahalaga ang pagiging bago ng data.

 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}
  • Ipinapakita ng code na ito ang halimbawa ng network-first. Kung may sagot sa network, ise-save ito sa cache; kung wala, babalik sa cached version. Angkop ito para sa mga resource na kailangan ng sariwang data, gaya ng mga balitang artikulo o API na sagot.

stale-while-revalidate

Ang stale-while-revalidate ay ibinabalik muna ang cache at sabay na ina-update ito mula sa network sa background. Binabalanse nito ang bilis ng tugon at pagiging bago.

 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}
  • Ang code na ito ay agad nagbabalik ng cache kapag meron, habang tahimik ang pagkuha ng bagong datos mula sa network para i-update ang cache. Nagbibigay ito ng mabilis na response sa users at ginagamit ang updated na content sa susunod na access, kaya angkop para sa UI o magaan na paghahatid ng data.

Pag-optimize ng Daloy ng Update (Update Notification at Ligtas na Reload)

Ang mga update ng Service Worker ay hindi agad-agad; ang bagong bersyon ay mananatiling naghihintay hangga't bukas pa ang mga kasalukuyang tab.

Dito natin ipinatutupad ang sistemang mag-aabiso sa client kung handa na ang bagong version at ligtas na magre-reload ng page ayon sa aksyon ng user.

Ipaalam sa client mula sa panig ng Service Worker kapag handa na ang bagong bersyon.

 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});
  • Sa code na ito, tinatawag ang notifyClientsUpdated sa dulo ng activate event upang abisuhan ang lahat ng naka-konektang client na handa na ang bagong version. Ang clients.claim() ay isang pamamaraan na agad nagdadala sa mga kasalukuyang bukas na pahina (mga client) sa ilalim ng kontrol ng bagong-activate na Service Worker. Karaniwan, ang Service Worker ay nagsisimulang kontrolin ang pahina sa susunod na load lamang, ngunit sa paggamit ng clients.claim(), maaari mong agad makontrol ang pahina nang hindi nire-reload.

Ipakita ang update UI sa client, at i-reload ang page sa aksyon ng user

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});
  • Tumatanggap ang client ng SW_UPDATED sa pamamagitan ng message event at ipinapakita ang abiso ng update sa UI. Kapag pinili ng user na i-reload, e-execute ang `window.location.reload()`, ini-update ang mga luma nang HTML, CSS, at iba pang resources sa pahina sa pinakabagong bersyon. Tinitiyak nito na ang cache at ang kontrol ng `Service Worker` na nailipat gamit ang `clients.claim()` ay naipapakita sa buong pahina.

Offline Fallback

Maghanda ng /offline.html para sa mahalagang pag-navigate, at magbigay ng minimal na UI na may sapat na impormasyon kahit walang larawan o font. Kapag nabigo ang API call, ipakita ang huling naka-cache na estado kung maaari at subukang mag-refetch sa background para mapabuti ang UX.

Halimbawa ng Pagpapatupad

 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});
  • I-precache ang /offline.html habang nasa install event upang may maibalik kahit minimal na pahina kapag walang network.
  • Sa `fetch` event, maaari mong subaybayan ang mga navigation request gamit ang `request.mode === 'navigate'` at tiyak na i-target ang mga paglipat ng pahina.
  • Mag-fallback sa /offline.html kapag nabigo ang network, upang sigurado na may lalabas na pahina kahit walang internet.

Pagpapalitan ng mensahe sa pagitan ng client at ng Service Worker

Dahil ang `Service Worker` ay gumagana nang hiwalay sa page lifecycle, mahalaga ang bidirectional messaging para mag-abiso ng mga estado at magsagawa ng mga utos. Ang pag-define ng mga uri para sa mga mensahe ay nakakatulong upang maiwasan ang maling pagpapadala ng mensahe, nagbibigay ng code completion, at nagpapalakas ng iyong implementasyon.

Halimbawa ng Code

  1. Paglalarawan ng Mga Uri ng Mensahe
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 };
  • Ang SwToClient ay uri ng mensahe na ipinapadala mula Service Worker papuntang client.
  • Ang ClientToSw ay uri ng mensahe na ipinapadala mula client papunta sa Service Worker.
  • Dahil dito, maipapaliwanag mo nang malinaw ang mga uri ng event na maaaring ipalitan sa pamamagitan ng two-way na komunikasyon.
  1. Pagpoproseso sa Bahagi ng 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});
  • Tumatanggap ang Service Worker ng mga mensahe mula sa client at hinahati ang proseso depende sa uri ng mensahe.
  • Para sa CLEAR_CACHE, binubura nito ang cache saka ipinapaalam sa lahat ng client gamit ang CACHE_CLEARED.
  • Para sa PING, sumasagot ito sa orihinal na client ng PING na mensahe na may kasamang timestamp.
  1. Magaabiso sa Lahat ng Client mula sa 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}
  • Gamitin ang clients.matchAll para makuha lahat ng window tab.
  • Sa pagpapadala ng postMessage sa bawat isa, maaari kang mag-broadcast ng mga mensahe.
  • Magagamit ito sa update notification (gaya ng SW_UPDATED) at error notification.
  1. Pagpoproseso sa Bahagi ng Client
1navigator.serviceWorker.controller?.postMessage({
2  type: 'PING',
3  ts: Date.now()
4} as ClientToSw);
  • Sa pamamagitan ng pagpapadala ng `PING` mula sa kliyente at pagtanggap ng tugon mula sa `Service Worker`, maaari mong matiyak na maayos na gumagana ang two-way na komunikasyon. Napapadali nito ang pag-test ng mga koneksyon at paghawak ng mga mensahe.
 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 の更新やリロード、通知表示などを行います。^}

Mga Benepisyo ng Typed Messaging

  • Ang paggamit ng typed messages ay nagpapalinaw kung alinsong mga mensahe ang maaaring ipadala at matanggap, at pinapabuti ng auto-completion at type checking ang kaligtasan.
  • Pinapaganang ng postMessage ang one-to-one na komunikasyon at pinapaganang ng broadcast ang one-to-many na komunikasyon.
  • Madali mong maisasama ang mahahalagang features tulad ng update notifications (SW_UPDATED), cache management (CACHE_CLEARED), at health check (PING).

Buod

  • Ang paggamit ng TypeScript ay nagdadagdag ng type safety sa Service Worker API at messaging, kaya napapabuti nang husto ang development efficiency at maintainability.
  • Ang pag-unawa sa install, activate, at fetch na lifecycle events, at pagpili ng tamang caching strategy (tulad ng cache-first o network-first) para sa bawat sitwasyon ay nagdadala ng mas magandang karanasan sa user.
  • Para sa mga operasyon, mahalagang maintindihan ang cache version management at update flow (updatefound, waiting, SKIP_WAITING, atbp.).
  • Sa pamamagitan ng paggamit ng typed messaging para sa komunikasyon ng client at Service Worker, maiiwasan mo ang maling pagpapatupad at makakapagtatag ka ng sistemang madaling palawakin at panatilihin sa pangmatagalang panahon.

Maaari mong sundan ang artikulo sa itaas gamit ang Visual Studio Code sa aming YouTube channel. Paki-check din ang aming YouTube channel.

YouTube Video