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

Jake Archibald
Jake Archibald

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

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

הכוונה

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

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

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

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

בקצרה:

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

מתבצע רישום של קובץ שירות (service worker) ואחריו תמונה של כלב אחרי 3 שניות.

הנה קובץ השירות שלו, 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) בכרטיסיית האפליקציה:

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

התקנה

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

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

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

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

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

לפשט את הפיתוח

מחזור החיים של 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 הידע הזה ייתן לכם ביטחון רב יותר כשתפרוסו ותעדכנו את Service Workers.