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

Jake Archibald
Jake Archibald

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

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

הכוונה

המטרה של מחזור החיים היא:

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

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

קובץ השירות הראשון (service worker)

בקצרה:

  • האירוע install הוא האירוע הראשון שנשלח ל-Service Worker, והוא מתרחש רק פעם אחת.
  • הבטחה שמועברת אל installEvent.waitUntil() מציינת את משך ההתקנה, את ההצלחה או את הכישלון שלה.
  • קובץ שירות (service worker) לא יקבל אירועים כמו 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. עם זאת, אם מריצים את הדוגמה שלמעלה, רואים כלב בפעם הראשונה שטוענים את הדף. לוחצים על 'רענון' ורואים את החתול.

היקף ושליטה

היקף ברירת המחדל של רישום של קובץ שירות (service worker) הוא ./ ביחס לכתובת ה-URL של הסקריפט. כלומר, אם רושמים קובץ שירות (service worker) ב-//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

התקנה

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

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

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

הפעלה

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

בפעם הראשונה שטוענים את ההדגמה, למרות שנשלחה בקשה אל dog.svg זמן רב לאחר שה-Service Worker מופעל, הבקשה לא טופלה, ועדיין אפשר לראות את התמונה של הכלב. ברירת המחדל היא עקביות. אם הדף נטען ללא Service Worker, לא גם משאבי המשנה שלו. אם טוענים את ההדגמה שוב (במילים אחרות, מרעננים את הדף), השליטה בידיים שלכם. הדף וגם התמונה יעברו אירועי 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 של קובץ השירות השתנתה. עם זאת, צריך להימנע משינוי כתובת ה-URL של העובד.
  • כברירת מחדל, רוב הדפדפנים, כולל Chrome 68 ואילך, מתעלם מכותרות שמירה במטמון בעת חיפוש עדכונים של הסקריפט הרשום של Service Worker. הם עדיין פועלים בהתאם לכותרות השמירה במטמון בעת אחזור משאבים שנטענים בתוך קובץ שירות (service worker) דרך importScripts(). כדי לשנות את התנהגות ברירת המחדל הזו, אפשר להגדיר את האפשרות updateViaCache במהלך רישום ה-Service Worker.
  • ה-Service Worker נחשב כעדכני אם הוא שונה בבייט מהקובץ שכבר נמצא בדפדפן. (אנחנו מרחיבים את ההגדרה הזו כך שתכלול גם סקריפטים/מודולים מיובאים.)
  • קובץ השירות המעודכן מופעל יחד עם ה-Service Worker הקיים, ומקבלים אירוע install משלו.
  • אם לעובד החדש יש קוד סטטוס לא תקין (לדוגמה, 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 המעודכן ישהה את ההפעלה עד שה-Service Worker הקיים יפסיק לשלוט בלקוחות. המצב הזה נקרא 'בהמתנה', וכך הדפדפן מוודא שרק גרסה אחת של Service Worker פועלת בכל פעם.

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

כלי פיתוח שמציגים קובץ שירות חדש (service worker) שממתין

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

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

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

הפעלה

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

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

אם מעבירים את ההבטחה ל-event.waitUntil(), תתבצע אגירת נתונים של אירועים שקשורים למאגר הנתונים (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) כדי לעדכן את קובץ השירות. אוף.

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

פיתוח קל יותר

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

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

זה הסרטון המועדף עליי.

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

הפעולה הזו תשנה את מחזור החיים לידידותי למפתחים. בכל ניווט:

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

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

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

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

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

Shift-טעינה מחדש

אם טוענים מחדש את הדף (Shift-reload) באופן מאולץ, הפעולה עוקפת לגמרי את ה-Service Worker. המכשיר לא יהיה בשליטתו. התכונה הזו היא במפרט ולכן היא פועלת בדפדפנים אחרים שתומכים ב-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.
});

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

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