חייו של קובץ שירות (service worker)

קשה לדעת מה עושים קובצי שירות (service worker) בלי להבין את מחזור החיים שלהם. אופן הפעולה שלהם ייראה אטום, ואפילו שרירותי. כדאי לזכור, בדומה לכל API אחר בדפדפן, התנהגויות של Service Worker מוגדרות היטב ומוגדרות היטב ומאפשרות להפעיל אפליקציות אופליין, ובמקביל מאפשרות לבצע עדכונים בלי לפגוע בחוויית המשתמש.

לפני שנכנסים ל-Workbox, חשוב להבין את מחזור החיים של Service Worker כדי שיהיה הגיוני להשתמש ב-Workbox.

הגדרת מונחים

לפני שנכנסים למחזור החיים של קובץ השירות (service worker), כדאי להגדיר כמה מונחים סביב אופן הפעולה של מחזור החיים.

בקרה והיקף

השליטה היא חיונית להבנת האופן שבו פועלים קובצי שירות (service worker). דף שמתואר כנשלט על ידי קובץ שירות (service worker) הוא דף שמאפשר ל-Service Worker ליירט בקשות רשת מטעמו. קובץ השירות (service worker) נמצא וכן יכול לבצע פעולות בדף במסגרת היקף נתון.

היקף

ההיקף של קובץ השירות (service worker) נקבע על סמך המיקום שלו בשרת אינטרנט. אם קובץ שירות (service worker) פועל בדף שנמצא ב-/subdir/index.html וממוקם בכתובת /subdir/sw.js, ההיקף של קובץ השירות הוא /subdir/. כדי לראות את העיקרון של היקף בפועל, עיין בדוגמה הבאה:

  1. עוברים אל https://service-worker-scope-viewer.glitch.me/subdir/index.html. תופיע הודעה שבה כתוב 'ה-Service Worker לא שולט בדף'. אבל דף זה רושם קובץ שירות (service worker) מ-https://service-worker-scope-viewer.glitch.me/subdir/sw.js.
  2. לטעון מחדש את הדף. מאחר שה-Service Worker רשום ופעיל עכשיו, הוא שולט בדף. יוצג לכם טופס שכולל את ההיקף, המצב הנוכחי וכתובת ה-URL של קובץ השירות. הערה: הטעינה מחדש של הדף לא קשורה להיקף, אלא למחזור החיים של קובץ השירות (service worker), שיוסבר בהמשך.
  3. עכשיו עוברים אל https://service-worker-scope-viewer.glitch.me/index.html. למרות שה-Service Worker רשום במקור הזה, עדיין מוצגת הודעה שלפיה אין קובץ שירות (service worker) כרגע. הסיבה לכך היא שהדף הזה לא נמצא בטווח של קובץ השירות הרשום.

היקף ההרשאות מגביל את הדפים שבשליטת קובץ השירות (service worker). בדוגמה הזו, קובץ השירות (service worker) שנטען מ-/subdir/sw.js יכול לשלוט רק בדפים שנמצאים ב-/subdir/ או בעץ המשנה שלו.

כפי שמתואר למעלה, ההיקפים פועלים כברירת מחדל, אבל אפשר לשנות את ההיקף המקסימלי המותר על ידי הגדרת כותרת התגובה Service-Worker-Allowed והעברת אפשרות scope ל-method register.

אלא אם יש סיבה טובה מאוד להגביל את ההיקף של קובץ השירות (service worker) לקבוצת משנה של מקור, לטעון את Service Worker מספריית ה-Root של שרת האינטרנט כדי שההיקף שלו יהיה רחב ככל האפשר, בלי לדאוג לגבי הכותרת Service-Worker-Allowed. ככה זה הרבה יותר פשוט לכולם.

לקוח

כשאומרים ש-Service Worker שולט בדף, הוא למעשה שולט בלקוח. לקוח הוא כל דף פתוח שכתובת ה-URL שלו נמצאת בתחום של קובץ השירות (service worker). באופן ספציפי, אלה מופעים של WindowClient.

מחזור החיים של קובץ שירות (service worker) חדש

כדי שה-Service Worker יוכל לשלוט בדף, הוא צריך להיות קיים תחילה. קודם כול נראה מה קורה כשפורסים קובץ שירות (service worker) חדש באתר שאין לו קובץ שירות פעיל.

הרשמה

הרישום הוא השלב הראשוני במחזור החיים של קובץ השירות (service worker):

<!-- In index.html, for example: -->
<script>
  // Don't register the service worker
  // until the page has fully loaded
  window.addEventListener('load', () => {
    // Is service worker available?
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js').then(() => {
        console.log('Service worker registered!');
      }).catch((error) => {
        console.warn('Error registering service worker:');
        console.warn(error);
      });
    }
  });
</script>

הקוד הזה פועל ב-thread הראשי ומבצע את הפעולות הבאות:

  1. מכיוון שהביקור הראשון של משתמש באתר ללא קובץ שירות (service worker) רשום, עליכם להמתין עד שהדף ייטען במלואו לפני שרושמים קובץ שירות. כך ניתן למנוע תחרות על רוחב הפס אם ה-Service Worker שומר מראש פריט כלשהו.
  2. אף על פי שיש תמיכה טובה ב-Service Worker, בדיקה מהירה תעזור למנוע שגיאות בדפדפנים שבהם הוא לא נתמך.
  3. כשהדף נטען בצורה מלאה, ואם יש תמיכה ב-Service Worker, צריך לרשום את /sw.js.

הנה מספר דברים שחשוב להבין:

  • קובצי שירות (service worker) זמינים רק ב-HTTPS או ב-localhost.
  • אם התוכן של Service Worker כולל שגיאות תחביר, הרישום נכשל וה-Service Worker נמחק.
  • תזכורת: קובצי שירות (service worker) פועלים במסגרת היקף מסוים. כאן, ההיקף הוא המקור כולו, כפי שהוא נטען מספריית השורש.
  • בתחילת הרישום, מצב קובץ השירות (service worker) מוגדר ל-'installing'.

בסיום ההרשמה, ההתקנה מתחילה.

התקנה

קובץ שירות (service worker) מפעיל את האירוע install שלו לאחר הרישום. האפליקציה install מופעלת רק פעם אחת לכל קובץ שירות (service worker), והיא לא תופעל שוב עד שיעודכן. ניתן לרשום קריאה חוזרת (callback) לאירוע install בהיקף העובד באמצעות addEventListener:

// /sw.js
self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v1';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v1'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.bc7b80b7.css',
      '/css/home.fe5d0b23.css',
      '/js/home.d3cc4ba4.js',
      '/js/jquery.43ca4933.js'
    ]);
  }));
});

הפעולה הזו יוצרת מופע חדש של Cache ושומרת נכסים דיגיטליים במטמון. בהמשך יהיו לנו הרבה הזדמנויות לדבר על יצירה מראש, אז בואו נתמקד בתפקיד של event.waitUntil. event.waitUntil מאשר את ההבטחה וממתין עד לפתרון הבעיה של ההבטחה. בדוגמה הזו, ההבטחה הזו מבצעת שתי פעולות אסינכרוניות:

  1. יצירת מופע חדש של Cache בשם 'MyFancyCache_v1'.
  2. אחרי שיוצרים את המטמון, מערך של כתובות URL של נכסים נשמר מראש באמצעות השיטה addAll האסינכרונית שלו.

ההתקנה נכשלת אם ההבטחות שהועברו אל event.waitUntil נדחו. במקרה כזה, ה-Service Worker נמחק.

אם ההבטחות resolve, ההתקנה תצליח והמצב של קובץ השירות (service worker) ישתנה ל-'installed' ולאחר מכן יופעל.

Activation (הפעלה)

אם הרישום וההתקנה יסתיימו בהצלחה, ה-Service Worker יופעל והמצב שלו יהפוך ל'activating' אפשר לבצע עבודה במהלך ההפעלה באירוע activate של ה-Service Worker. משימה אופיינית באירוע הזה היא להסיר קבצים שמורים ישנים, אבל ל-Service Worker חדש לגמרי זה לא רלוונטי כרגע ונרחיב בהמשך כשנדבר על עדכונים של Service Worker.

עבור קובצי שירות (service worker) חדשים, activate מופעל מיד לאחר ביצוע הפעולה install. בסיום ההפעלה, המצב של קובץ השירות (service worker) הופך ל-'activated'. שימו לב שכברירת מחדל, קובץ השירות (service worker) החדש לא יתחיל לשלוט בדף עד לניווט הבא או לרענון הדף.

טיפול בעדכונים של Service Worker

אחרי הפריסה של קובץ השירות (service worker) הראשון, כנראה שיהיה צורך לעדכן אותו מאוחר יותר. לדוגמה, יכול להיות שיידרש עדכון אם קורים שינויים בטיפול בבקשות או בלוגיקה של טרום-מטמון.

מתי מתבצעים עדכונים

דפדפנים יבדקו אם יש עדכונים ל-Service Worker כאשר:

  • המשתמש מנווט לדף שנמצא בטווח של קובץ השירות (service worker).
  • הקריאה ל-navigator.serviceWorker.register() מתבצעת באמצעות כתובת URL ששונה מה-Service Worker שכבר מותקן – אבל אסור לשנות את כתובת ה-URL של קובץ השירות (service worker)!
  • הקריאה ל-navigator.serviceWorker.register() מופעלת עם אותה כתובת URL של קובץ השירות (service worker) המותקן, אבל עם היקף אחר. שוב, כדי להימנע מכך, יש להשאיר את ההיקף בשורש של המקור, אם אפשר.
  • כאשר אירועים כמו 'push' או 'sync' הופעלו ב-24 השעות האחרונות, אבל עוד לא צריך לדאוג לגבי האירועים האלה.

איך מתבצעים עדכונים

חשוב לדעת מתי הדפדפן מעדכן את Service Worker, אבל גם חשוב לדעת "איך". בהנחה שכתובת ה-URL או ההיקף של קובץ השירות לא השתנו, כאשר קובץ השירות (service worker) שמותקן עכשיו, הוא מתעדכן לגרסה חדשה רק אם התוכן שלו השתנה.

דפדפנים מזהים שינויים בכמה דרכים:

  • שינויים בכל בייט-לבייט בסקריפטים שהתבקשו על ידי importScripts, אם רלוונטי.
  • כל שינוי בקוד ברמה העליונה של Service Worker משפיע על טביעת האצבע שהדפדפן יצר.

הדפדפן עושה כאן עבודה רבה. כדי להבטיח שבדפדפן יש את כל מה שהוא צריך כדי לזהות באופן מהימן שינויים בתוכן של קובץ שירות (service worker), אל תבקשו למטמון ה-HTTP לשמור אותו ואל תשנו את שם הקובץ. הדפדפן מבצע בדיקות עדכון באופן אוטומטי כשיש ניווט לדף חדש בטווח של Service Worker.

הפעלה ידנית של בדיקות עדכונים

בדרך כלל, לוגיקת הרישום לא אמורה להשתנות בנוגע לעדכונים. עם זאת, יוצא מן הכלל אחד עשוי להיות אם פעילויות באתר נמשכות ממושך. זה יכול לקרות באפליקציות בדף יחיד, שבהן בקשות ניווט הן נדירות, כי האפליקציה בדרך כלל נתקלת בבקשת ניווט אחת בתחילת מחזור החיים של האפליקציה. במצבים כאלה, ייתכן שיופעל עדכון ידני ב-thread הראשי:

navigator.serviceWorker.ready.then((registration) => {
  registration.update();
});

באתרים מסורתיים, או בכל מקרה שבו הסשנים של המשתמשים לא נמשכים זמן רב, סביר להניח שאין צורך להפעיל עדכונים ידניים.

התקנה

כשמשתמשים ב-bundler כדי ליצור נכסים סטטיים, הנכסים האלה יכילו גיבובים בשמם, למשל framework.3defa9d2.js. נניח שחלק מהנכסים האלה נשמרו מראש לצורך גישה במצב אופליין מאוחר יותר. לשם כך נדרש עדכון של קובץ השירות (service worker) כדי לשמור מראש את הנכסים שעודכנו במטמון:

self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v2';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v2'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.ced4aef2.css',
      '/css/home.cbe409ad.css',
      '/js/home.109defa4.js',
      '/js/jquery.38caf32d.js'
    ]);
  }));
});

יש שני דברים שונים מהדוגמה הראשונה לאירוע install מהדוגמה הקודמת:

  1. המערכת יוצרת מופע חדש של Cache עם מפתח מסוג 'MyFancyCacheName_v2'.
  2. שמות הנכסים שנשמרו מראש השתנו.

חשוב לשים לב לכך ש-Service Worker מעודכן מותקן לצד ה-Service Worker הקודם. כלומר, ל-Service Worker הישן עדיין יש שליטה על כל הדפים הפתוחים, ולאחר ההתקנה הקוד החדש ייכנס למצב המתנה עד שהוא יופעל.

כברירת מחדל, קובץ שירות (service worker) חדש יופעל כאשר הלקוח הישן לא שולט. המצב הזה מתרחש כשכל הכרטיסיות הפתוחות של האתר הרלוונטי נסגרות.

Activation (הפעלה)

כשמתקינים Service Worker מעודכן ושלב ההמתנה מסתיים, הוא מופעל וה-Service Worker הישן נמחק. משימה נפוצה שצריך לבצע באירוע activate של קובץ שירות (service worker) מעודכנת היא להסיר קבצים שמורים ישנים. כדי להסיר קבצים שמורים ישנים, אפשר להשיג את המפתחות לכל המכונות הפתוחות של Cache באמצעות caches.keys ולמחוק קבצים שמורים שלא נמצאים ברשימת הרשאות מוגדרת עם caches.delete:

self.addEventListener('activate', (event) => {
  // Specify allowed cache keys
  const cacheAllowList = ['MyFancyCacheName_v2'];

  // Get all the currently active `Cache` instances.
  event.waitUntil(caches.keys().then((keys) => {
    // Delete all caches that aren't in the allow list:
    return Promise.all(keys.map((key) => {
      if (!cacheAllowList.includes(key)) {
        return caches.delete(key);
      }
    }));
  }));
});

מטמון ישן לא מוסר את עצמם. אנחנו צריכים לעשות זאת בעצמנו או להסתכן בחריגה ממכסות האחסון. מכיוון שהקובץ 'MyFancyCacheName_v1' מה-Service Worker הראשון לא מעודכן, רשימת ההרשאות במטמון מתעדכנת כך שתציין את 'MyFancyCacheName_v2'. הפעולה הזו גורמת למחיקת מטמון בעל שם אחר.

האירוע activate יסתיים לאחר הסרת המטמון הישן. בשלב הזה, ה-Service Worker החדש יכסה את השליטה בדף ויחליף בסופו של דבר את הדף הישן!

מחזור החיים נמשך כל הזמן

בין אם אפשר להשתמש ב-Workbox לטיפול בפריסה ובעדכונים של Service Worker או להשתמש ישירות ב-Service Worker API, משתלם להבין את מחזור החיים של Service Worker. מתוך ההבנה הזו, ההתנהגות של Service Worker אמורה להיראות יותר הגיונית מאשר מסתורית.

כדי להתעמק בנושא הזה, כדאי לקרוא את המאמר הזה מאת ג'ייק ארצ'יבלד. יש המון ניואנסים בכל הנוגע לריקוד לאורך כל מחזור החיים של השירות, אבל כן אפשר לדעת זאת, והידע הזה יכול להיות שימושי מאוד כשמשתמשים ב-Workbox.