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

Jake Archibald
Jake Archibald

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

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

הכוונה

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

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

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

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

בקצרה:

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

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

שגיאה מוצגת בכרטיסייה של כלי הפיתוח של קובץ השירות (Service Worker)

התקנה

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

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

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

הפעלה

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

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

clients.claim

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

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

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

עדכון ה-service worker

בקצרה:

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

נניח ששינינו את הסקריפט של ה-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

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

זה עלול לגרום לבעיה כמו זו:

  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;

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

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 בביטחון רב יותר.