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

אם ה-promise נדחה, סימן שההתקנה נכשלה והדפדפן יטמיע את ה-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 שולט בחלק מהלקוחות שנטענו בלי clients.claim().

עדכון ה-service worker

בקצרה:

  • עדכון מופעל אם מתרחש אחד מהמקרים הבאים:
    • ניווט לדף שנכלל בהיקף הבדיקה.
    • אירועים פונקציונליים כמו push ו-sync, אלא אם בוצעה בדיקה של עדכון ב-24 השעות האחרונות.
    • קריאה ל-.register() רק אם כתובת ה-URL של ה-service worker השתנתה. עם זאת, אין לשנות את כתובת ה-URL של העובד.
  • ברירת המחדל של רוב הדפדפנים, כולל Chrome מגרסה 68 ואילך, היא להתעלם מכותרות של שמירת מטמון כשבודקים אם יש עדכונים לסקריפט של ה-service worker הרשום. הם עדיין מכבדים כותרות של שמירת נתונים במטמון כשהם מאחזרים משאבים שנטענו בתוך קובץ שירות דרך importScripts(). כדי לשנות את התנהגות ברירת המחדל הזו, מגדירים את האפשרות updateViaCache כשרושמים את ה-service worker.
  • ה-service worker נחשב מעודכן אם הוא שונה ב-byte מה-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 פועלת בכל פעם.

אם הפעלתם את הדמו המעודכן, עדיין אמורה להופיע תמונה של חתול, כי עדיין לא הפעלתם את העובד של 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;

אם יש לכם עובד שממתין, אתם יכולים ללחוץ על 'דילוג על ההמתנה' ב-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 בביטחון רב יותר.