מחזור החיים של ה-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 בכרטיסיית האפליקציה:
התקנה
האירוע הראשון שעובד שירות מקבל הוא 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:
גם אם יש לכם רק כרטיסייה אחת פתוחה עם הדמו, רענון הדף לא מספיק כדי לאפשר לגרסה החדשה להשתלט. הסיבה לכך היא האופן שבו פועלים ניווטים בדפדפנים. כשמנווטים, הדף הנוכחי לא נעלם עד שמתקבלות כותרות התשובה, וגם אז יכול להיות שהדף הנוכחי יישאר אם לתשובה יש כותרת 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 ייחודית. לא לעשות את זה! בדרך כלל לא מומלץ לעשות זאת בשירותי עובדים. פשוט מעדכנים את הסקריפט במיקום הנוכחי שלו.
זה עלול לגרום לבעיה כמו זו:
index.html
רושם אתsw-v1.js
כקובץ שירות (service worker).sw-v1.js
שומר בזיכרון ומציג אתindex.html
, כך שהוא פועל קודם במצב אופליין.- מעדכנים את
index.html
כדי שיירשםsw-v2.js
החדש והבריא.
אם מבצעים את הפעולות שלמעלה, המשתמש אף פעם לא מקבל את sw-v2.js
, כי sw-v1.js
מציג את הגרסה הישנה של index.html
מהמטמון שלו. הגעתם למצב שבו אתם צריכים לעדכן את ה-service worker כדי לעדכן את ה-service worker. איכס.
עם זאת, בהדגמה שלמעלה, שיניתי את כתובת ה-URL של ה-service worker. כך תוכלו לעבור בין הגרסאות במהלך הדגמה. זה לא משהו שאעשה בסביבת הייצור.
פיתוח קל
מחזור החיים של קובץ השירות נבנה מתוך מחשבה על המשתמש, אבל במהלך הפיתוח הוא קצת מסובך. למרבה המזל, יש כמה כלים שיעזרו לכם:
עדכון בזמן טעינה מחדש
זה המועדף עליי.
כך מחזור החיים הופך לידידותי למפתחים. כל ניווט:
- מאחזרים מחדש את ה-service worker.
- מתקינים אותו כגרסה חדשה גם אם הוא זהה בייט-בייט, כלומר אירוע
install
פועל והמטמון מתעדכן. - מדלגים על שלב ההמתנה כדי שקובץ השירות החדש יופעל.
- מנווטים בדף.
המשמעות היא שתקבלו את העדכונים בכל פעם שתבצעו פעולת ניווט (כולל רענון) בלי שתצטרכו לטעון מחדש פעמיים או לסגור את הכרטיסייה.
דילוג על ההמתנה
אם יש לכם עובד שממתין, אתם יכולים ללחוץ על 'דילוג על ההמתנה' ב-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 בביטחון רב יותר.