Service Worker w TypeScript
Ten artykuł wyjaśnia Service Workery w TypeScript.
Wyjaśnimy Service Workery w TypeScript, włącznie z praktycznymi przykładami.
YouTube Video
Service Worker w TypeScript
Service Worker to „pełnomocnik żądań” działający między przeglądarką a siecią. Umożliwia przechwytywanie żądań fetch, zarządzanie pamięcią podręczną, wsparcie offline oraz przetwarzanie w tle (synchronizacja i push). Użycie TypeScript zapewnia bezpieczeństwo typów i poprawia łatwość utrzymania kodu.
Konfigurowanie TypeScript
tsconfig.json
(Włącz typy WebWorker)
Zobaczmy przykład włączenia typu WebWorker w pliku 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}
- Dodając
WebWorker
do tablicylib
, możesz korzystać z takich typów jakServiceWorkerGlobalScope
. DOM
iWebWorker
mają różne typy, więc powszechną praktyką jest rozdzielanie ustawieńtsconfig.json
dla przeglądarki (główna aplikacja) iService Worker
a.Pliki Service Workera są ostatecznie zapisywane w **ścieżce odpowiadającej zasięgowi** (zazwyczaj główny katalog strony, czyli
/sw.js`).- Ze względów bezpieczeństwa Service Workery działają tylko przez HTTPS (lub na
localhost
).
Kod rejestracji po stronie przeglądarki
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 );
- Ten proces rejestruje
Service Workera
.scope
oznacza zakres ścieżek, które może kontrolować Service Worker. Na przykład, jeśli umieścisz/sw.js
bezpośrednio w katalogu głównym i ustawiszscope
na katalog główny (/
), możesz kontrolować wszystkie zasoby całej strony. Z drugiej strony, jeśli określisz konkretny katalog, taki jak/app/
, kontrolowana będzie tylko zawartość tego katalogu.
1 // If there's a waiting worker, notify the user.
2 if (registration.waiting) {
3 promptUserToUpdate(registration);
4 }
waiting
oznacza stan, w którym nowy Service Worker został zainstalowany i oczekuje na aktywację. Na tym etapie istniejące strony są nadal kontrolowane przez starego Service Workera, więc zazwyczaj prosi się użytkownika o potwierdzenie, a po uzyskaniu zgody wywołuje sięskipWaiting()
, aby natychmiast aktywować nowego Service Workera. Pozwala to zastosować najnowsze zmiany bez czekania na ponowne załadowanie strony.
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
jest wywoływane gdy rozpocznie się instalacja nowego Service Workera. Gdy to zdarzenie wystąpi, nowy worker zostaje ustawiony wregistration.installing
, więc monitorując jegostatechange
, można wykryć moment zakończenia instalacji (installed
). Dodatkowo, jeśli istniejenavigator.serviceWorker.controller
, oznacza to, że stara wersja Service Workera już kontroluje stronę, co daje możliwość poinformowania użytkownika o istnieniu nowej wersji.
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}
- Zdarzenie
controllerchange
występuje w momencie, gdy nowy Service Worker zaczyna kontrolować aktualną stronę. Ponowne załadowanie w tym momencie natychmiast zastosuje nowe strategie cache oraz przetwarzania. Jednak automatyczne przeładowanie może pogorszyć doświadczenie użytkownika, dlatego lepiej przeładować stronę po uzyskaniu zgody użytkownika.
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();
- Wysyłając do Service Workera wiadomość
postMessage({ type: 'SKIP_WAITING' })
po stronie klienta, a następnie wywołującself.skipWaiting()
, można wymusić aktualizację.
Deklaracja zakresu w sw.ts
Następnie przyjrzyjmy się typowemu przykładzie Service Workera wdrażającemu caching „app shell”.
Przy użyciu Service Workera w TypeScript warto przypisać właściwy typ do self
.
1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
- W TypeScript
self
jest domyślnie traktowane jakoany
, więc bez dodatkowego typowania nie uzyskasz uzupełniania typów ani sprawdzania typów dla specyficznych API Service Workera, takich jakskipWaiting()
lubclients
. - Określenie typu
ServiceWorkerGlobalScope
pozwala na podpowiedzi autouzupełniania, zapobiega błędom i umożliwia bezpieczniejszy rozwój oddzielony od standardowych skryptów DOM.
Podstawowy Service Worker (Install/Activate/Fetch)
Pokazuje zarządzanie wersjami pamięci podręcznej, wstępne cachowanie podczas instalacji, usuwanie starych cache podczas aktywacji oraz strategie cachowania przy fetch (najpierw cache dla zasobów statycznych, najpierw sieć dla API).
sw.ts
(Minimalna konfiguracja + szkielet 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});
- Podczas zdarzenia
install
statyczne zasoby aplikacji (App Shell) są wstępnie cachowane. Wywołującself.skipWaiting()
, nowyService Worker
jest aktywowany natychmiast, dzięki czemu najnowszy cache jest dostępny bez konieczności czekania na kolejne wejście.
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});
- Podczas zdarzenia
activate
stare wersje cache są usuwane, aService Worker
jest aktualizowany. Ponadto, wywołującself.clients.claim()
, nowyService Worker
może przejąć kontrolę nad wszystkimi klientami bez konieczności przeładowania strony.
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});
- W
fetch
możesz przechwytywać żądania i kontrolować odpowiedzi. Możesz wdrożyć strategie takie jak cache-first lub network-first, przydatne dla wsparcia offline i wydajności.
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});
- Po otrzymaniu
SKIP_WAITING
, wywołanieself.skipWaiting()
pozwala natychmiastowo aktywować oczekującego Service Workera. W rezultacie nowa wersja zostanie zastosowana przy następnym żądaniu, bez potrzeby przeładowania strony.
Przegląd praktycznych strategii cachowania
cache-first
Cache-first najpierw sprawdza pamięć podręczną i zwraca odpowiedź natychmiast, jeśli jest dostępna. W przeciwnym razie pobiera z sieci i zapisuje odpowiedź w pamięci podręcznej. To podejście jest odpowiednie dla plików statycznych.
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}
- Ten kod demonstruje implementację cache-first. Jeśli istnieje wpis w pamięci podręcznej, jest on zwracany; jeśli nie, następuje pobranie z sieci i zapisanie do cache. To dobre rozwiązanie dla statycznych zasobów, które rzadko się zmieniają, takich jak obrazy czy CSS.
network-first
Network-first najpierw próbuje pobrać z sieci, a w przypadku niepowodzenia wraca do cache. To dobre podejście dla API, gdzie ważna jest świeżość danych.
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}
- Ten kod demonstruje implementację network-first. Jeśli otrzymano odpowiedź z sieci, jest ona zapisywana w cache; jeśli wystąpi błąd, zwracana jest wersja z cache. Nadaje się do zasobów wymagających aktualnych danych, jak artykuły newsowe czy odpowiedzi API.
stale-while-revalidate
stale-while-revalidate
najpierw zwraca cache, a jednocześnie w tle aktualizuje go z sieci. To pozwala zrównoważyć szybkość odpowiedzi i świeżość danych.
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 natychmiast zwraca cache, jeśli jest dostępny, a w tle pobiera nowe dane z sieci, by zaktualizować pamięć podręczną. Zapewnia szybkie odpowiedzi użytkownikom i korzysta z odświeżonej zawartości przy kolejnym dostępie, dzięki czemu sprawdza się przy UI lub lekkim przesyle danych.
Optymalizacja procesu aktualizacji (powiadamianie i bezpieczne przeładowanie)
Aktualizacje Service Workera nie są natychmiastowe; nowa wersja będzie oczekiwać do momentu zamknięcia istniejących kart.
Tutaj wdrażamy system powiadamiający klienta, gdy nowa wersja jest gotowa oraz bezpiecznie przeładowujący stronę po akcji użytkownika.
Powiadom klienta po stronie Service Workera, gdy nowa wersja jest gotowa.
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});
- W tym kodzie
notifyClientsUpdated
jest wywoływane na końcu zdarzeniaactivate
, by powiadomić wszystkich połączonych klientów o gotowości nowej wersji.clients.claim()
to metoda, która natychmiast obejmuje kontrolą nowo aktywowanego Service Workera wszystkie aktualnie otwarte strony (klientów). Zwykle Service Worker zaczyna kontrolować stronę dopiero przy następnym załadowaniu, ale używającclients.claim()
, można objąć stronę kontrolą natychmiast, bez przeładowania.
Wyświetl UI powiadamiające o aktualizacji po stronie klienta i przeładuj stronę po akcji użytkownika
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});
- Klient odbiera
SW_UPDATED
przez zdarzeniemessage
i wyświetla powiadomienie o aktualizacji w UI. Gdy użytkownik wybierze ponowne załadowanie, wykonywane jestwindow.location.reload()
, co aktualizuje stare HTML, CSS i inne zasoby na stronie do najnowszej wersji. To zapewnia, że cache oraz kontrola przezService Workera
przejęta za pomocąclients.claim()
są odzwierciedlone na całej stronie.
Tryb offline (fallback)
Przygotuj /offline.html
na wypadek krytycznej nawigacji i zapewnij minimalny UI, który przekazuje treści nawet bez obrazów czy czcionek. Jeśli wywołanie API zakończy się niepowodzeniem, pokaż ostatni zapisany stan z cache i spróbuj pobrać dane ponownie w tle, by poprawić UX.
Przykład implementacji
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});
- Zacachuj
/offline.html
podczas zdarzeniainstall
, by móc zwrócić przynajmniej podstawową stronę przy braku sieci. - Podczas zdarzenia
fetch
można monitorować żądania nawigacji przy użyciurequest.mode === 'navigate'
, aby precyzyjnie obsługiwać przejścia między stronami. - Jeśli sieć zawiedzie, wróć do
/offline.html
, by zapewnić wyświetlanie nawet w trybie offline.
Komunikacja pomiędzy klientem a Service Workerem
.
Ponieważ Service Worker
działa niezależnie od cyklu życia strony, dwukierunkowa komunikacja jest ważna do przekazywania stanów i wykonywania poleceń. Określenie typów wiadomości pomaga zapobiec błędnemu wysyłaniu wiadomości, umożliwia uzupełnianie kodu i sprawia, że implementacja jest bardziej odporna.
Przykład kodu
- Definicja typów wiadomości
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
to typ wiadomości wysyłanej od Service Workera do klienta.ClientToSw
to typ wiadomości wysyłanej od klienta do Service Workera.- Pozwala to precyzyjnie określić typy zdarzeń, które mogą być wymieniane za pomocą dwukierunkowej komunikacji.
- Przetwarzanie po stronie Service Workera
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 odbiera wiadomości od klienta i rozgałęzia przetwarzanie w zależności od typu.
- Dla
CLEAR_CACHE
usuwa cache, a następnie powiadamia wszystkich klientów przezCACHE_CLEARED
. - Dla
PING
odsyła klientowi wiadomośćPING
zawierającą znacznik czasu.
- Powiadamianie wszystkich klientów z Service Workera
1async function broadcast(msg: SwToClient) {
2 const clients = await self.clients.matchAll({ includeUncontrolled: true });
3 for (const c of clients) c.postMessage(msg);
4}
- Użyj
clients.matchAll
, by pobrać wszystkie otwarte karty/przeglądarki. - Wysyłając
postMessage
do każdej, możesz rozgłaszać wiadomości. - Możesz to wykorzystać do powiadomień o aktualizacjach (np.
SW_UPDATED
) czy powiadomieniach o błędach.
- Przetwarzanie po stronie klienta
1navigator.serviceWorker.controller?.postMessage({
2 type: 'PING',
3 ts: Date.now()
4} as ClientToSw);
- Wysyłając
PING
z klienta i otrzymując odpowiedź odService Workera
, możesz zweryfikować, czy dwukierunkowa komunikacja działa poprawnie. To ułatwia testowanie stanu połączenia oraz obsługi wiadomości.
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 の更新やリロード、通知表示などを行います。^}
Korzyści z typowanej komunikacji
- Typowanie wiadomości sprawia, że jasno wiadomo, jakie wiadomości można wysyłać i odbierać, a podpowiedzi kodu i sprawdzanie typów zwiększają bezpieczeństwo.
postMessage
umożliwia komunikację jeden do jednego, abroadcast
– jeden do wielu.- Możesz łatwo wdrożyć niezbędne funkcjonalności, takie jak powiadomienia o aktualizacjach (
SW_UPDATED
), zarządzanie pamięcią (CACHE_CLEARED
) oraz sprawdzanie stanu (PING
).
Podsumowanie
- Użycie TypeScript dodaje bezpieczeństwo typów do wywołań API Service Workera i komunikacji, znacznie poprawiając efektywność rozwoju i łatwość utrzymania.
- Zrozumienie cyklu życia (zdarzenia
install
,activate
,fetch
) oraz dobór odpowiedniej strategii cachowania (jak cache-first lub network-first) poprawia doświadczenie użytkownika. - Dla operacji istotne jest zrozumienie zarządzania wersjami cache oraz przebiegu aktualizacji (
updatefound
,waiting
,SKIP_WAITING
itd.). - Stosowanie komunikacji typowanej pomiędzy klientem a Service Workerem pozwala zapobiec błędnej implementacji oraz tworzy system łatwy do rozszerzania i utrzymania w dłuższej perspektywie.
Możesz śledzić ten artykuł, korzystając z Visual Studio Code na naszym kanale YouTube. Proszę również sprawdzić nasz kanał YouTube.