Service Worker ב-TypeScript
מאמר זה מסביר על Service Workers ב-TypeScript.
נסביר על Service Workers ב-TypeScript, כולל דוגמאות מעשיות.
YouTube Video
Service Worker ב-TypeScript
Service Worker הוא "פרוקסי של בקשות" שיושב בין הדפדפן לרשת. הוא מאפשר יירוט של fetch, שליטה על מטמון, תמיכה לאופליין ועיבוד ברקע (סינכרון ודחיפה). שימוש ב-TypeScript מספק בטיחות טיפוסים ומגביר את התחזוקתיות.
הגדרת TypeScript
tsconfig.json
(הפעלת טיפוסים של WebWorker)
בואו נסתכל על דוגמה להפעלת טיפוס WebWorker בתוך 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}
- בהוספת
WebWorker
למערךlib
, תוכלו להשתמש בטיפוסים כמוServiceWorkerGlobalScope
. DOM
ו-WebWorker
הם בעלי טיפוסים שונים, ולכן נהוג להפריד את ההגדרות שלtsconfig.json
בין הדפדפן (האפליקציה הראשית) ל-Service Worker
.- קבצי
Service Worker
נוצרים בסופו של דבר בנתיב שתואם את ה-scope (בדרך כלל בשורש האתר/sw.js
). - מסיבות אבטחה, Service Workers פועלים רק מעל HTTPS (או על
localhost
).
קוד הרשמה בצד הדפדפן
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 );
- תהליך זה רושם
Service Worker
.scope
מתייחס לטווח הנתיבים שה-Service Worker
יכול לשלוט בהם. לדוגמה, אם תמקם את/sw.js
ישירות בשורש ותגדיר את ה-scope
לתיקיית השורש (/
), תוכל לשלוט בכל המשאבים של האתר כולו. מצד שני, אם תציין תיקיה מסוימת כמו/app/
, רק התוכן תחת תיקיה זו יהיה תחת שליטה.
1 // If there's a waiting worker, notify the user.
2 if (registration.waiting) {
3 promptUserToUpdate(registration);
4 }
waiting
מציין את המצב בו ה-Service Worker
החדש הותקן וממתין להפעלה. בשלב זה, הדפים הקיימים עדיין נשלטים על ידי ה-Service Worker
הישן, ולכן נהוג להציג למשתמש אישור, ולאחר קבלת האישור לקרוא ל-skipWaiting()
כדי להפעיל מיד את ה-Service Worker
החדש. כך תוכל להחיל את התהליכים החדשים מיידית, מבלי להמתין לטעינת דף מחדש.
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
מתרחש כאשר מתקיניםService Worker
חדש. כאשר אירוע זה מתרחש, פועל חדש נשמר ב-registration.installing
, ועל ידי מעקב אחרי השינוי במצב (statechange
), תוכל לזהות מתי ההתקנה הושלמה (installed
). בנוסף, אםnavigator.serviceWorker.controller
קיים, המשמעות היא ש-Service Worker ישן כבר שולט בדף, ולכן זו הזדמנות להודיע למשתמש על קיומה של גרסה חדשה.
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}
- האירוע
controllerchange
מופעל כאשר ה-Service Worker
החדש מתחיל לשלוט בדף הנוכחי. טעינת הדף מחדש בשלב זה תיישם מיד את אסטרטגיות המטמון החדשות והעיבוד החדש. עם זאת, טעינה אוטומטית עלולה לפגוע בחוויית המשתמש, לכן עדיף לבצע טעינה מחודשת לאחר קבלת אישור מהמשתמש.
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();
- אם ה-
Service Worker
מקבלpostMessage({ type: 'SKIP_WAITING' })
מהלקוח ואז קורא ל-self.skipWaiting()
, ניתן לעדכן את ה-Service Worker מיד.
הכרזת Scope ב-sw.ts
כעת נבחן דוגמה טיפוסית של Service Worker שמיישמת caching של app shell.
בעת שימוש ב-Service Workers ב-TypeScript, כדאי להגדיר את הטיפוס הנכון ל-self
.
1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
- ב-TypeScript,
self
מטופל כברירת מחדל כ-any
, ולכן ללא טיפוס נוסף, לא תקבל השלמת טיפוסים או בדיקת טיפוס עבור API-ים ייעודיים ל-Service Worker כמוskipWaiting()
אוclients
. - הגדרת הטיפוס
ServiceWorkerGlobalScope
מאפשרת השלמה אוטומטית, מונעת שימוש לקוי ומספקת פיתוח בטוח ונפרד מסקריפטים רגילים של DOM.
Service Worker בסיסי (Install/Activate/Fetch)
היא מציגה ניהול גרסאות פשוט של cache, precaching בהתקנה, מחיקת cache ישנים באקטיבציה ואסטרטגיות מטמון (cache-first לנכסים סטטיים, network-first ל-APIים).
sw.ts
(הגדרה מינימלית + מבנה מטמון)
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});
- במהלך אירוע
install
, משאבים סטטיים של האפליקציה (App Shell) נשמרים בזיכרון מטמון מראש. על ידי קריאה ל-self.skipWaiting()
, ה-Service Worker
החדש מופעל מיד, והמטמון המעודכן זמין מיידית מבלי להמתין לביקור הבא.
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});
- באירוע
activate
, גרסאות ישנות של מטמון נמחקות וה-Service Worker
נשמר מעודכן. כמו כן, על ידי קריאה ל-self.clients.claim()
, ה-Service Worker
החדש יכול לשלוט בכל הלקוחות ללא צורך ברענון הדף.
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});
- ב-
fetch
אפשר ליירט בקשות ולשלוט בתגובה. ניתן ליישם אסטרטגיות כמו cache-first או network-first, החשובות לתמיכה באופליין ולביצועים.
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});
- אם מתקבל
SKIP_WAITING
, קריאה ל-self.skipWaiting()
תאפשר להפעיל מיידית את ה-Service Worker שנמצא במצב המתנה. כתוצאה מכך, הגרסה החדשה תחול כבר בבקשה הבאה, מבלי צורך לטעון את הדף מחדש.
סקירה של אסטרטגיות מטמון מעשיות
cache-first
Cache-first בודקת תחילה את המטמון ומחזירה תגובה אם היא קיימת. אם לא, מבצעת fetch מהרשת ומטמנת את התוצאה. זה מתאים לקבצים סטטיים.
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}
- הקוד הזה מדגים יישום של cache-first. אם יש מטמון – מחזיר אותו; ואם לא – מושך מהאינטרנט ושומר במטמון. מתאים למשאבים סטטיים שכמעט ולא משתנים, כמו תמונות או CSS.
network-first
Network-first מנסה קודם מהרשת ואם נכשל – חוזר למטמון. זה מתאים ל-APIים בהם חשובה העדכניות.
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}
- הקוד הזה מדגים יישום של network-first. אם מתקבלת תגובה מהרשת – שומר במטמון; אם נכשל – מחזיר את הגרסה מהמטמון. מתאים למשאבים שדורשים מידע עדכני, כמו כתבות חדשות או תגובות API.
stale-while-revalidate
stale-while-revalidate מחזיר תחילה את המטמון ומעדכן אותו במקביל מהרשת ברקע. זה מאזין בין מהירות תגובה לרעננות הנתונים.
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}
- הקוד הזה מחזיר את המטמון במידת הצורך, ומעדכן נתונים חדשים מהרשת ברקע כדי לעדכן את המטמון. זה מספק תגובה מהירה למשתמשים ומשתמש בתוכן עדכני בגישה הבאה – אידיאלי לממשק משתמש או משלוח נתונים קל.
אופטימיזציה של תהליך העדכון (הודעת עדכון ורענון בטוח)
עדכוני Service Worker
אינם מיידיים; הגרסה החדשה תישאר במצב המתנה עד לסגירת כל הלשוניות הקיימות.
כאן נממש מערכת שמודיעה ללקוח כשהגרסה החדשה מוכנה ומבצעת רענון מאובטח לפי בחירת המשתמש.
הודע ללקוח מהצד של ה-Service Worker
כאשר הגרסה החדשה מוכנה
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});
- בקוד הזה,
notifyClientsUpdated
נקרא בסיום אירועactivate
כדי להודיע לכל הלקוחות שהגרסה החדשה מוכנה.clients.claim()
היא מתודה שמעבירה מיד את כל הדפים (לקוחות) הפתוחים לשליטת ה-Service Worker החדש שהופעל. בדרך כלל,Service Worker
מתחיל לשלוט בדף רק בטעינה הבאה, אך באמצעותclients.claim()
ניתן להעביר את השליטה מיידית ללא צורך בטעינה חוזרת.
הצג ממשק התראה על עדכון בצד הלקוח, ורענן את הדף בפעולת המשתמש
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});
- הלקוח מקבל את
SW_UPDATED
דרך אירועmessage
ומציג למשתמש התראה על עדכון. כאשר המשתמש בוחר לרענן, מתבצעת קריאה ל-window.location.reload()
, מה שמעדכן את ה-HTML, CSS ומשאבים נוספים לגרסה האחרונה. דבר זה מבטיח שמטמון ושליטה על ידי ה-Service Worker
שהוחלף באמצעותclients.claim()
משתקפים בכל הדף.
נפילה (Fallback) לאופליין
הכן את /offline.html
עבור ניווט קריטי, וספק ממשק משתמש מינימלי שמעביר מסר גם ללא תמונות או פונטים. אם קריאת API נכשלת, הצג את המצב האחרון מהמטמון אם אפשר ונסה לשלוף מהרשת ברקע לשיפור חוויית המשתמש.
דוגמת מימוש
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});
- מטמינים מראש את
/offline.html
במהלך אירועinstall
כדי שתוכל להחזיר לפחות עמוד מינימלי כשהרשת אינה זמינה. - באירוע
fetch
, ניתן לעקוב אחרי בקשות ניווט באמצעותrequest.mode === 'navigate'
ולפנות ישירות למעבר בין דפים. - חזור ל-
/offline.html
כאשר הרשת נכשלת, וכך תבטיח שיוצג גם באופליין.
תקשורת הודעות בין הלקוח ל-Service Worker
מכיוון ש-Service Worker
פועל באופן עצמאי ממחזור החיים של הדף, תקשורת דו־כיוונית חשובה להודעה על מצבים ולהפעלת פקודות. קביעת טיפוסים להודעות עוזרת למנוע שליחת הודעות שגויות, מאפשרת השלמת קוד, והופכת את היישום שלך לעמיד יותר.
דוגמת קוד
- הגדרת טיפוסי הודעות
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
הוא טיפוס ההודעה שנשלחת מ-Service Worker אל הלקוח.ClientToSw
הוא טיפוס ההודעה שנשלחת מהלקוח אל ה-Service Worker.- באמצעות זאת תוכל להבהיר אילו סוגי אירועים ניתן להחליף בתקשורת דו-כיוונית.
- עיבוד בצד ה-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 מקבל הודעות מהלקוח ומבצע עיבוד בהתאם לטיפוס ההודעה.
- עבור
CLEAR_CACHE
, הוא מוחק את המטמון ואז מודיע לכל הלקוחות עםCACHE_CLEARED
. - עבור
PING
, הוא משיב ללקוח המקורי עם הודעתPING
הכוללת חותמת זמן.
- הודעה לכל הלקוחות מתוך 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}
- השתמש ב-
clients.matchAll
כדי לקבל את כל הטאבים של החלון. - באמצעות שליחת
postMessage
לכל אחד, ניתן לשדר הודעות לכל הלשוניות. - ניתן להשתמש בכך להודעות עדכון (
SW_UPDATED
) ולהודעות שגיאה.
- עיבוד בצד הלקוח
1navigator.serviceWorker.controller?.postMessage({
2 type: 'PING',
3 ts: Date.now()
4} as ClientToSw);
- על ידי שליחת
PING
מהלקוח וקבלת תגובה מ-Service Worker
, ניתן לוודא שתקשורת דו-כיוונית פועלת כראוי. כך קל יותר לבדוק מצבי חיבור וטיפול בהודעות.
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 の更新やリロード、通知表示などを行います。^}
יתרונות של תקשורת הודעות עם טיפוסים
- שימוש בהודעות עם טיפוסים מבהיר איזה הודעות אפשר לשלוח ולקבל, והשלמה אוטומטית ובדיקות טיפוס משפרים את הבטיחות.
postMessage
מאפשר תקשורת אחד לאחד, ו-broadcast
מאפשר תקשורת אחד לרבים.- אפשר ליישם בקלות תכונות חשובות כמו התראות עדכון (
SW_UPDATED
), ניהול מטמון (CACHE_CLEARED
) ובדיקות תקינות (PING
).
סיכום
- שימוש ב-TypeScript מוסיף בטיחות טיפוסים לקריאות API של Service Worker ולתעבורת הודעות, ובכך משפר משמעותית את יעילות והתוחזקות הפיתוח.
- הבנה של אירועי מחזור החיים
install
,activate
ו-fetch
, ובחירה באסטרטגיית המטמון הנכונה (כמו cache-first או network-first) לכל מצב – מביאה לחוויית משתמש טובה יותר. - לתפעול תקין, חשוב להבין ניהול גרסאות של המטמון ותהליכי עדכון (
updatefound
,waiting
,SKIP_WAITING
ועוד). - באמצעות תקשורת מבוססת טיפוסים בין הלקוח ל-
Service Worker
, ניתן למנוע טעויות וליצור מערכת שקל להרחיב ולתחזק אותה בטווח הארוך.
תוכלו לעקוב אחר המאמר שלמעלה באמצעות Visual Studio Code בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.