Service Worker dalam TypeScript

Service Worker dalam TypeScript

Artikel ini menerangkan tentang Service Worker dalam TypeScript.

Kami akan menerangkan tentang Service Worker dalam TypeScript, termasuk contoh praktikal.

YouTube Video

Service Worker dalam TypeScript

Service Worker ialah “proksi permintaan” yang terletak di antara pelayar dan rangkaian. Ia membolehkan pemintas fetch, kawalan cache, sokongan luar talian, dan pemprosesan latar belakang (sync dan push). Menggunakan TypeScript memberikan keselamatan jenis dan meningkatkan kebolehselenggaraan.

Menetapkan TypeScript

tsconfig.json (Aktifkan jenis WebWorker)

Mari kita lihat contoh cara mengaktifkan jenis WebWorker dalam 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 menambah WebWorker ke dalam senarai lib, anda boleh menggunakan jenis seperti ServiceWorkerGlobalScope.
  • DOM dan WebWorker mempunyai jenis yang berbeza, jadi adalah amalan biasa untuk memisahkan tetapan tsconfig.json untuk pelayar (aplikasi utama) dan Service Worker.
  • Fail Service Worker akhirnya dihasilkan ke laluan yang sepadan dengan skop (biasanya akar laman /sw.js).
  • Atas sebab keselamatan, Service Worker hanya berjalan melalui HTTPS (atau pada localhost).

Kod Pendaftaran di Sisi Pelayar

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 merujuk kepada julat laluan yang boleh dikawal oleh Service Worker. Sebagai contoh, jika anda meletakkan /sw.js terus di bawah akar dan menetapkan scope ke direktori akar (/), anda boleh mengawal semua sumber di seluruh laman web. Sebaliknya, jika anda menentukan direktori tertentu seperti /app/, hanya kandungan di bawah direktori tersebut yang akan dikawal.
1    // If there's a waiting worker, notify the user.
2    if (registration.waiting) {
3      promptUserToUpdate(registration);
4    }
  • waiting menunjukkan keadaan di mana Service Worker baharu telah dipasang dan sedang menunggu untuk diaktifkan. Pada peringkat ini, halaman sedia ada masih dikawal oleh Service Worker lama, jadi adalah biasa untuk meminta pengesahan pengguna, dan selepas mendapat kelulusan, panggil skipWaiting() untuk segera mengaktifkan Service Worker baharu. Ini membolehkan anda memaparkan proses terbaharu tanpa menunggu halaman dimuat semula.
 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 dicetuskan apabila pemasangan Service Worker baharu telah dimulakan. Apabila peristiwa ini berlaku, worker baharu ditetapkan dalam registration.installing, jadi dengan memantau statechange-nya, anda boleh mengesan bila pemasangan telah selesai (installed). Selain itu, jika navigator.serviceWorker.controller wujud, ini bermakna Service Worker lama sudah mengawal halaman tersebut, jadi ini adalah peluang untuk memberitahu pengguna tentang kewujudan 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 dicetuskan pada saat Service Worker baharu mula mengawal halaman semasa. Memuat ulang pada ketika ini akan serta-merta menggunakan strategi cache dan pemprosesan baharu. Walau bagaimanapun, pemuatan semula secara automatik boleh menjejaskan pengalaman pengguna, jadi adalah lebih baik untuk memuat semula selepas 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 memastikan Service Worker menerima postMessage({ type: 'SKIP_WAITING' }) dari klien dan kemudian memanggil self.skipWaiting(), anda boleh mendorong kemaskini.

Pengisytiharan Skop di dalam sw.ts

Seterusnya, mari lihat contoh biasa Service Worker yang melaksanakan cache app shell.

Apabila menggunakan Service Worker dalam TypeScript, adalah berguna untuk menetapkan jenis yang betul pada self.

1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
  • Dalam TypeScript, self dianggap sebagai any secara lalai, jadi tanpa penetapan jenis tambahan, anda tidak akan mendapat penyelesaian jenis atau semakan jenis untuk API khusus Service Worker seperti skipWaiting() atau clients.
  • Menentukan ServiceWorkerGlobalScope membolehkan auto-completion, mencegah salah guna, dan membenarkan pembangunan yang lebih selamat berasingan dari skrip DOM biasa.

Service Worker Asas (Install/Activate/Fetch)

Ia menunjukkan pengurusan versi cache yang ringkas, precache semasa install, memadam cache lama semasa activate, dan strategi cache semasa fetch (cache-first untuk aset statik, network-first untuk API).

sw.ts (Tetapan Minimum + Rangka 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});
  • Semasa acara install, sumber statik (App Shell) aplikasi telah dicache terlebih dahulu. Dengan memanggil self.skipWaiting(), Service Worker baru akan diaktifkan serta-merta, menjadikan cache terkini tersedia tanpa perlu menunggu akses seterusnya.
 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});
  • Dalam acara activate, versi cache lama akan dipadamkan, dan Service Worker dikekalkan agar sentiasa terkini. Selain itu, dengan memanggil self.clients.claim(), Service Worker baru boleh mengawal semua klien tanpa perlu menunggu halaman dimuat semula.
 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});
  • Dalam fetch, anda boleh memintas permintaan dan mengawal respons. Anda boleh melaksanakan strategi seperti cache-first atau network-first, sesuai untuk sokongan luar talian dan prestasi.
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() membolehkan anda mengaktifkan Service Worker yang sedang menunggu dengan segera. Hasilnya, versi baharu akan diterapkan pada permintaan seterusnya tanpa perlu memuat semula halaman.

Gambaran Keseluruhan Strategi Cache Praktikal

cache-first

Cache-first menyemak cache dahulu dan memulangkan respons serta-merta jika tersedia. Jika tiada, ia mengambil dari rangkaian dan menyimpan hasilnya dalam cache. Ini sesuai untuk fail statik.

 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}
  • Kod ini menunjukkan pelaksanaan cache-first. Jika ada cache, ia akan mengembalikan cache tersebut; jika tiada, ia akan mengambil dari rangkaian dan menyimpannya dalam cache. Ia sesuai untuk sumber statik yang jarang berubah, seperti imej atau CSS.

network-first

Network-first mencuba rangkaian dahulu dan kembali kepada cache jika gagal. Ini sesuai untuk API yang memerlukan maklumat terkini.

 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}
  • Kod ini menunjukkan pelaksanaan network-first. Jika respons rangkaian diterima, ia disimpan ke cache; jika gagal, ia mengembalikan versi yang terdapat dalam cache. Ia sesuai untuk sumber yang memerlukan data terkini, seperti artikel berita atau respons API.

stale-while-revalidate

stale-while-revalidate akan mengembalikan cache terlebih dahulu dan serentak mengemaskininya dari rangkaian di latar belakang. Ini mengimbangi kelajuan respons dan kesegaran maklumat.

 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}
  • Kod ini mengembalikan cache dengan serta-merta jika ada, sambil memuat turun data baharu dari rangkaian di latar belakang untuk mengemas kini cache. Ia memberikan respons yang pantas kepada pengguna dan menggunakan kandungan terkini untuk akses berikutnya, sesuai untuk UI atau penghantaran data ringan.

Mengoptimumkan Aliran Kemas Kini (Notifikasi Kemas Kini dan Muat Semula yang Selamat)

Kemas kini Service Worker bukan segera; versi baharu akan kekal menunggu sehingga tab sedia ada ditutup.

Di sini, kami melaksanakan sistem untuk memaklumkan klien apabila versi terbaru telah sedia dan memuat semula halaman dengan selamat berdasarkan tindakan pengguna.

Maklumkan klien dari pihak Service Worker apabila versi baharu 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});
  • Dalam kod ini, notifyClientsUpdated dipanggil pada penghujung acara activate untuk memberitahu semua klien yang berhubung bahawa versi baru telah sedia. clients.claim() ialah kaedah yang dengan serta-merta meletakkan halaman (klien) yang sedang dibuka di bawah kawalan Service Worker baharu yang diaktifkan. Kebiasaannya, Service Worker mula mengawal halaman hanya pada pemuatan seterusnya, tetapi dengan menggunakan clients.claim(), anda boleh segera mengawal halaman tanpa perlu memuat semula.

Papar UI kemas kini pada klien, dan muat semula apabila pengguna bertindak

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 acara message dan memaparkan notifikasi kemas kini dalam UI. Apabila pengguna memilih untuk memuat semula, window.location.reload() akan dilaksanakan, mengemaskini HTML, CSS, dan sumber lain yang lama di halaman kepada versi terkini. Ini memastikan cache dan kawalan oleh Service Worker yang ditukar dengan clients.claim() dipantulkan di seluruh halaman.

Fallback Luar Talian

Sediakan /offline.html untuk navigasi kritikal, dan sediakan UI minimum yang masih memberi makna walaupun tanpa imej atau fon. Jika panggilan API gagal, paparkan keadaan cache terakhir jika boleh dan cuba fetch semula di latar belakang untuk menambah baik UX.

Contoh Pelaksanaan

 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});
  • Precache /offline.html semasa acara install supaya sekurang-kurangnya anda boleh memaparkan halaman minimum apabila rangkaian tidak tersedia.
  • Dalam acara fetch, anda boleh memantau permintaan navigasi dengan request.mode === 'navigate' dan secara khusus menumpukan kepada peralihan halaman.
  • Fallback ke /offline.html apabila rangkaian gagal, memastikan ia tetap dipaparkan walaupun tanpa rangkaian.

Pemesejan antara klien dan Service Worker

Kerana Service Worker beroperasi secara bebas daripada kitaran hidup halaman, pemesejan dua hala adalah penting untuk memaklumkan status dan melaksanakan arahan. Menentukan jenis untuk mesej membantu mencegah penghantaran mesej yang salah, membolehkan pelengkapan kod, dan menjadikan implementasi anda lebih kukuh.

Contoh Kod

  1. Definisi Jenis Mesej
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 jenis mesej yang dihantar dari Service Worker ke klien.
  • ClientToSw adalah jenis mesej yang dihantar dari klien ke Service Worker.
  • Ini membolehkan anda memperjelas jenis peristiwa yang boleh dipertukarkan melalui komunikasi dua hala.
  1. Pemprosesan di pihak 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 mesej dari klien dan memproses mengikut jenis mesej.
  • Untuk CLEAR_CACHE, ia akan memadam cache dan kemudian memberitahu semua klien dengan CACHE_CLEARED.
  • Untuk PING, ia membalas kepada klien asal dengan mesej PING beserta cap masa.
  1. Maklumkan 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 mendapatkan semua tab tetingkap.
  • Dengan menghantar postMessage kepada setiap satu, anda boleh membuat siaran mesej.
  • Ini boleh digunakan untuk notifkasi kemas kini (seperti SW_UPDATED) dan pemberitahuan ralat.
  1. Pemprosesan di Pihak Klien
1navigator.serviceWorker.controller?.postMessage({
2  type: 'PING',
3  ts: Date.now()
4} as ClientToSw);
  • Dengan menghantar PING dari klien dan menerima respons daripada Service Worker, anda boleh mengesahkan bahawa komunikasi dua hala berfungsi dengan baik. Ini memudahkan ujian keadaan sambungan dan pengendalian mesej.
 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 の更新やリロード、通知表示などを行います。^}

Manfaat Pemesejan Bertip

  • Menggunakan mesej bertip menjadikan mesej yang boleh dihantar dan diterima lebih jelas, selain auto-completion dan semakan jenis meningkatkan keselamatan.
  • postMessage membolehkan komunikasi satu-ke-satu dan broadcast membolehkan komunikasi satu-ke-banyak.
  • Anda boleh melaksanakan ciri penting seperti notifikasi kemas kini (SW_UPDATED), pengurusan cache (CACHE_CLEARED), dan semakan status (PING) dengan mudah.

Ringkasan

  • Menggunakan TypeScript menambah keselamatan jenis pada panggilan API dan pemesejan Service Worker, sekaligus meningkatkan kecekapan pembangunan dan kebolehselenggaraan.
  • Memahami acara kitaran hayat install, activate, dan fetch, serta memilih strategi cache yang betul (seperti cache-first atau network-first) untuk setiap situasi akan membawa kepada pengalaman pengguna yang lebih baik.
  • Untuk operasi, memahami pengurusan versi cache dan aliran kemas kini (updatefound, waiting, SKIP_WAITING, dan lain-lain) adalah penting.
  • Dengan menggunakan pemesejan beraip untuk komunikasi antara klien dan Service Worker, anda dapat mengelakkan pelaksanaan yang salah dan membina sistem yang mudah dikembangkan serta diselenggara untuk jangka masa panjang.

Anda boleh mengikuti artikel di atas menggunakan Visual Studio Code di saluran YouTube kami. Sila lihat juga saluran YouTube kami.

YouTube Video