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

Jake Archibald
Jake Archibald

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

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

כוונת הרכישה

מטרת מחזור החיים היא:

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

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

קובץ השירות הראשון

בקצרה:

  • האירוע install הוא האירוע הראשון שנשלח ל-Service Worker, והוא מתרחש רק פעם אחת.
  • הבטחה שמועברת אל installEvent.waitUntil() מסמנת את משך ההתקנה ואת ההצלחה או הכישלון שלה.
  • אירועים כמו fetch ו-push לא יתקבלו על ידי עובד שירות עד שההתקנה שלו תסתיים בהצלחה והוא יהיה 'פעיל'.
  • כברירת מחדל, אחזור של דף לא יעבור דרך קובץ שירות (service worker), אלא אם בקשת הדף עצמה עברה דרך קובץ שירות. לכן, כדי לראות את ההשפעות של ה-service worker, צריך לרענן את הדף.
  • clients.claim() יכול לשנות את ברירת המחדל הזו ולקחת שליטה בדפים שלא נמצאים בשליטה.

הקוד הבא ב-HTML:

<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered!', reg))
    .catch(err => console.log('Boo!', err));

  setTimeout(() => {
    const img = new Image();
    img.src = '/dog.svg';
    document.body.appendChild(img);
  }, 3000);
</script>

מתבצע רישום של קובץ שירות ומוסיף תמונה של כלב אחרי 3 שניות.

זהו קובץ השירות (service worker) שלו, sw.js:

self.addEventListener('install', event => {
  console.log('V1 installing…');

  // cache a cat SVG
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/cat.svg'))
  );
});

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

המערכת שומרת במטמון תמונה של חתול ומציגה אותה בכל פעם שמתקבלת בקשה ל-/dog.svg. עם זאת, אם מריצים את הדוגמה שלמעלה, יופיע כלב בפעם הראשונה שיטעמו את הדף. מקישים על סמל הרענון ורואים את החתול.

היקף ושליטה

היקף ברירת המחדל של רישום של קובץ שירות הוא ./ ביחס לכתובת ה-URL של הסקריפט. כלומר, אם רושמים קובץ שירות ב-//example.com/foo/bar.js, הטווח שמוגדר כברירת מחדל הוא //example.com/foo/.

אנחנו קוראים לדפים, לעובדים ולעובדים משותפים: clients. ה-service worker יכול לשלוט רק בלקוחות שנמצאים בהיקף. אחרי שהלקוח 'נשלט', האחזורים שלו עוברים דרך ה-service worker ברמת ההיקף. אפשר לזהות אם הלקוח נשלט באמצעות navigator.serviceWorker.controller. הערך הזה יהיה null או מופע של Service Worker.

הורדה, ניתוח והפעלה

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

השגיאה מוצגת בכלי הפיתוח של Chrome במסוף ובקטע של ה-service worker בכרטיסיית האפליקציה:

מוצגת שגיאה בכרטיסייה &#39;כלי פיתוח&#39; של Service Worker

התקנה

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

האירוע install הוא ההזדמנות שלכם לשמור במטמון את כל מה שאתם צריכים לפני שתוכלו לשלוט בלקוחות. ההבטחה שאתם מעבירים ל-event.waitUntil() מאפשרת לדפדפן לדעת מתי ההתקנה הושלמה ואם היא הושלמה בהצלחה.

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

הפעלה

אחרי ש-service worker יהיה מוכן לשלוט בלקוחות ולטפל באירועים פונקציונליים כמו push ו-sync, תקבלו אירוע activate. אבל זה לא אומר שהדף שבו נקרא .register() יהיה בשליטה.

בפעם הראשונה שטענת את הדמו, למרות שהבקשה ל-dog.svg נשלחת הרבה אחרי שהשירות מופעל, הוא לא מטפל בבקשה ועדיין מוצגת התמונה של הכלב. ברירת המחדל היא עקביות. אם הדף נטען ללא שירות עובד, גם המשאבים המשניים שלו לא ייטענו. אם תטעינו את הדמו בפעם השנייה (כלומר, תריעננו את הדף), הוא יהיה בשליטה. בדף וגם בתמונה יופיעו אירועי fetch, ובמקום זאת יופיע חתול.

clients.claim

כדי לשלוט בלקוחות לא מבוקרים, אפשר להפעיל את clients.claim() בתוך ה-service worker אחרי שהוא מופעל.

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

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

עדכון ה-service worker

בקצרה:

  • עדכון מופעל אם מתרחש אחד מהמקרים הבאים:
    • ניווט לדף שנכלל בהיקף הבדיקה.
    • אירועים פעילים כמו push ו-sync, אלא אם הייתה בדיקת עדכונים ב-24 השעות האחרונות.
    • קריאה ל-.register() רק אם כתובת ה-URL של ה-service worker השתנתה. עם זאת, אין לשנות את כתובת ה-URL של העובד.
  • כברירת מחדל, רוב הדפדפנים, כולל Chrome 68 ואילך, מתעלם מכותרות שמירה במטמון בעת חיפוש עדכונים של הסקריפט הרשום של Service Worker. הם עדיין מכבדים כותרות של שמירת נתונים במטמון כשהם מאחזרים משאבים שנטענו בתוך קובץ שירות דרך importScripts(). כדי לשנות את התנהגות ברירת המחדל הזו, מגדירים את האפשרות updateViaCache כשרושמים את ה-service worker.
  • ה-service worker נחשב מעודכן אם הוא שונה בבייט מה-service worker שכבר קיים בדפדפן. (אנחנו מרחיבים את הבדיקה הזו גם לקבצים של סקריפטים או מודולים מיובאים).
  • קובץ השירות המעודכן מופעל יחד עם ה-Service Worker הקיים, ומקבלים אירוע install משלו.
  • אם לעובד החדש יש קוד סטטוס שאינו תקין (לדוגמה, 404), הוא לא מצליח לנתח, הוא גורם לשגיאה במהלך הביצוע או הוא נדחה במהלך ההתקנה, העובד החדש מושלך אבל העובד הנוכחי נשאר פעיל.
  • לאחר ההתקנה, העובד המעודכן wait עד שהעובד הקיים לא ישלוט באף לקוח. (לתשומת ליבכם: יש חפיפה בין לקוחות בזמן הרענון).
  • self.skipWaiting() מונע את ההמתנה, כלומר השירות העובד יופעל ברגע שההתקנה שלו תסתיים.

נניח ששינינו את הסקריפט של ה-service worker כך שיגיב עם תמונה של סוס במקום חתול:

const expectedCaches = ['static-v2'];

self.addEventListener('install', event => {
  console.log('V2 installing…');

  // cache a horse SVG into a new cache, static-v2
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  // delete any caches that aren't in expectedCaches
  // which will get rid of static-v1
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(() => {
      console.log('V2 now ready to handle fetches!');
    })
  );
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the horse SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

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

התקנה

לתשומת ליבך, שיניתי את שם המטמון מ-static-v1 ל-static-v2. המשמעות היא שאוכל להגדיר את המטמון החדש בלי לשכתב דברים במטמון הנוכחי, שבו עדיין משתמש ה-service worker הישן.

הדפוסים האלה יוצרים מטמון ספציפי לגרסה, בדומה לנכסים שאפליקציה נייטיב תאגד עם קובץ ההפעלה שלה. יכול להיות שיש לכם גם מטמון שהם לא ספציפיים לגרסה, כמו avatars.

בהמתנה

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

אם הפעלתם את הדמו המעודכן, עדיין אמורה להופיע תמונה של חתול, כי עדיין לא הפעלתם את העובד של V2. אפשר לראות את ה-service worker החדש שממתין בכרטיסייה 'אפליקציה' של DevTools:

DevTools שמוצגת בו הודעה על המתנה של שירות חדש

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

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

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

הפעלה

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

בהדגמה שלמעלה, אני מנהל/ת רשימה של מטמון שאמור להופיע שם, ובמקרה של activate אני נפטר מכל השאר, פעולה שמסירה את המטמון הישן של static-v1.

אם מעבירים ל-event.waitUntil() הבטחה (promise), היא תאגר ב-buffer אירועים פונקציונליים (fetch,‏ push,‏ sync וכו') עד שההבטחה תתבצע. לכן, כשהאירוע fetch מופעל, ההפעלה הושלמה באופן מלא.

דילוג על שלב ההמתנה

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

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

לא משנה מתי מתקשרים למספר skipWaiting(), כל עוד זה במהלך ההמתנה או לפניה. בדרך כלל קוראים לו באירוע install:

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

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

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

עדכונים ידניים

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

navigator.serviceWorker.register('/sw.js').then(reg => {
  // sometime later…
  reg.update();
});

אם אתם מצפים שהמשתמש ישתמש באתר שלכם למשך זמן רב בלי לטעון מחדש, מומלץ לקרוא ל-update() פרק זמן (למשל, מדי שעה).

הימנעו משינוי כתובת ה-URL של סקריפט ה-service worker

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

היא יכולה להוביל לבעיה כמו:

  1. index.html רושם את sw-v1.js כקובץ שירות (service worker).
  2. sw-v1.js שומר בזיכרון ומציג את index.html, כך שהוא פועל קודם במצב אופליין.
  3. מעדכנים את index.html כדי שיירשם sw-v2.js החדש והבריא.

אם מבצעים את הפעולות האלה, המשתמש אף פעם לא מקבל את sw-v2.js, כי sw-v1.js מציג את הגרסה הישנה של index.html מהמטמון שלו. הגעתם למצב שבו אתם צריכים לעדכן את ה-service worker כדי לעדכן את ה-service worker. איכס.

עם זאת, בהדגמה שלמעלה, שיניתי את כתובת ה-URL של ה-service worker. לכן, לצורך ההדגמה, ניתן לעבור בין הגרסאות. זה לא משהו שהייתי עושה בייצור.

פיתוח קל

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

עדכון בזמן טעינה מחדש

זה המועדף עליי.

כלי פיתוח שמוצגים בהם &#39;עדכון בטעינה מחדש&#39;

כך מחזור החיים הופך לידידותי למפתחים. כל ניווט:

  1. מאחזרים מחדש את ה-service worker.
  2. מתקינים אותו כגרסה חדשה גם אם הוא זהה בייט-בייט, כלומר אירוע install פועל והמטמון מתעדכן.
  3. מדלגים על שלב ההמתנה כדי שקובץ השירות החדש יופעל.
  4. מנווטים בדף.

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

דילוג על ההמתנה

כלי הפיתוח מציגים את ההודעה &#39;דילוג על ההמתנה&#39;

אם יש עובד שממתין, אפשר ללחוץ על 'דילוג בהמתנה' בכלי הפיתוח כדי לקדם אותו מיד ל'פעיל'.

Shift-reload

אם טוענים מחדש את הדף (Shift-reload) באופן מאולץ, הפעולה עוקפת לגמרי את ה-Service Worker. המכשיר לא יהיה בשליטתו. התכונה הזו היא במפרט ולכן היא פועלת בדפדפנים אחרים שתומכים ב-Service Worker.

טיפול בעדכונים

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

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

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // the installing worker, or undefined
  reg.waiting; // the waiting worker, or undefined
  reg.active; // the active worker, or undefined

  reg.addEventListener('updatefound', () => {
    // A wild service worker has appeared in reg.installing!
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - the install event has fired, but not yet complete
    // "installed"  - install complete
    // "activating" - the activate event has fired, but not yet complete
    // "activated"  - fully active
    // "redundant"  - discarded. Either failed install, or it's been
    //                replaced by a newer version

    newWorker.addEventListener('statechange', () => {
      // newWorker.state has changed
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // This fires when the service worker controlling this page
  // changes, eg a new worker has skipped waiting and become
  // the new active worker.
});

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

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