Service Worker ב-TypeScript

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 פועל באופן עצמאי ממחזור החיים של הדף, תקשורת דו־כיוונית חשובה להודעה על מצבים ולהפעלת פקודות. קביעת טיפוסים להודעות עוזרת למנוע שליחת הודעות שגויות, מאפשרת השלמת קוד, והופכת את היישום שלך לעמיד יותר.

דוגמת קוד

  1. הגדרת טיפוסי הודעות
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.
  • באמצעות זאת תוכל להבהיר אילו סוגי אירועים ניתן להחליף בתקשורת דו-כיוונית.
  1. עיבוד בצד ה-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 הכוללת חותמת זמן.
  1. הודעה לכל הלקוחות מתוך 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) ולהודעות שגיאה.
  1. עיבוד בצד הלקוח
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 בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.

YouTube Video