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

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

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

הגדרת מונחים

לפני שנכנסים למחזור החיים של 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 לשיטה register.

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

לקוח

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

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

כדי ש-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.

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

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

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

התקנה

קובץ שירות (service worker) מפעיל אירוע install לאחר ההרשמה. מתבצעת קריאה אחת בלבד אל install לכל קובץ שירות (service worker), והוא לא יופעל שוב עד לעדכון. אפשר לרשום קריאה חוזרת לאירוע 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 נמחק.

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

Activation (הפעלה)

אם הרישום וההתקנה יצליחו, כשה-Service Worker מופעל והמצב שלו הופך ל'activating' אפשר לבצע את העבודה בזמן ההפעלה אירוע אחד (activate). משימה אופיינית באירוע הזה היא להסיר מטמון ישן, אבל לגבי קובץ שירות חדש, לא רלוונטי כרגע, ונרחיב את השימוש בו כשנדבר על עדכונים של 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 המותקן, אבל בהיקף שונה. שוב, צריך להימנע מכך אם אפשר להשאיר את ההיקף ברמה הבסיסית (root) של המקור.
  • כאשר אירועים כמו 'push' או 'sync' הופעלו במהלך 24 השעות האחרונות, אבל עדיין לא צריך לדאוג לגבי האירועים האלה.

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

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

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

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

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

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

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

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

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

התקנה

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

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