מה הצוות של Bulletin למד על קובצי שירות (service worker) במהלך הפיתוח של אפליקציית PWA.
זהו הפוסטים הראשון בסדרה של פוסטים בבלוג על הלקחים שלמד צוות Google Bulletin במהלך פיתוח אפליקציית PWA חיצונית. בפוסט הזה נשתף כמה מהאתגרים שניצבנו בפניהם, את הגישות שבהן השתמשנו כדי להתגבר עליהם ואת העצות הכלליות שלנו להימנע ממלכודות. זוהי בהחלט לא סקירה כללית מלאה של אפליקציות PWA. המטרה היא לשתף את הלקחים מהניסיון של הצוות שלנו.
בפוסט הראשון הזה נספק קצת מידע רקע, ואז נצלול לכל מה שלמדנו על שירותי העבודה.
רקע
Bulletin היה בפיתוח פעיל מאמצע 2017 עד אמצע 2019.
למה בחרנו לפתח PWA
לפני שנצלול לתהליך הפיתוח, נבדוק למה פיתוח אפליקציית PWA הייתה אפשרות אטרקטיבית לפרויקט הזה:
- יכולת לבצע איטרציות במהירות. זה חשוב במיוחד כי אנחנו מתכננים להפעיל את Bulletin בפיילוט במספר שווקים.
- בסיס קוד יחיד. המשתמשים שלנו התחלקו באופן שווה פחות או יותר בין Android לבין iOS. בעזרת אפליקציית PWA, הצלחנו ליצור אפליקציית אינטרנט אחת שתעבוד בשתי הפלטפורמות. כך הצלחנו להאיץ את קצב העבודה ואת ההשפעה של הצוות.
- מתעדכנים במהירות ולא תלויים בהתנהגות המשתמשים. אפליקציות PWA יכולות להתעדכן באופן אוטומטי, וכך להפחית את מספר הלקוחות עם הגרסאות הלא מעודכנות. הצלחנו להשיק שינויים משמעותיים לקצה העורפי עם זמן העברה קצר מאוד ללקוחות.
- שילוב קל עם אפליקציות צד ראשון וצד שלישי השילובים האלה היו דרישה לאפליקציה. באפליקציות PWA, בדרך כלל המשמעות היא פשוט פתיחת כתובת URL.
- הסרת הבעיה בהתקנת אפליקציה
המסגרת שלנו
ב-Bulletin השתמשנו ב-Polymer, אבל כל מסגרת מודרנית עם תמיכה טובה תעשה את העבודה.
מה למדנו על קובצי שירות
אי אפשר להשתמש ב-PWA בלי service worker. קובצי שירות (service workers) מעניקים לכם הרבה כוח, כמו אסטרטגיות מתקדמות של שמירת נתונים במטמון, יכולות אופליין, סנכרון ברקע וכו'. אמנם קובצי שירות מוסיפים קצת מורכבות, אבל מצאנו שהיתרונות שלהם עולים על המורכבות הנוספת.
אם אפשר, יוצרים אותו
מומלץ לא לכתוב סקריפט של service worker באופן ידני. כשיוצרים קובצי שירות באופן ידני, צריך לנהל באופן ידני את המשאבים ששמורים במטמון ולכתוב מחדש את הלוגיקה שקיימת ברוב הספריות של קובצי השירות, כמו Workbox.
עם זאת, בגלל סטאק הטכנולוגיה הפנימי שלנו, לא יכולנו להשתמש בספרייה כדי ליצור ולנהל את ה-service worker שלנו. הלקחים שלנו בהמשך ישקפו את זה לפעמים. למידע נוסף, אפשר לקרוא את המאמר מלכודות בקובצי שירות (service workers) שלא נוצרו.
לא כל הספריות תואמות ל-Service Worker
בספריות JS מסוימות יש הנחות שלא פועלות כצפוי כשהן מופעלות על ידי שירות עבודה. לדוגמה, אם window
או document
זמינים, או אם משתמשים ב-API שלא זמין לעובדים בשירות (XMLHttpRequest
, אחסון מקומי וכו'). חשוב לוודא שכל הספריות הקריטיות שנחוצות לאפליקציה תואמות ל-service worker. ב-PWA הספציפי הזה, רצינו להשתמש ב-gapi.js לאימות, אבל לא הצלחנו כי לא הייתה תמיכה בקובצי שירות (service workers). בנוסף, מחברי הספריות צריכים לצמצם או להסיר הנחות מיותרות לגבי ההקשר של JavaScript, במידת האפשר, כדי לתמוך בתרחישי שימוש של שירותי עבודה. למשל, כדאי להימנע מממשקי API שלא תואמים לשירותי עבודה ולהימנע ממצב גלובלי.
הימנעות מגישה ל-IndexedDB במהלך האתחול
אל תקראו את IndexedDB בזמן האיניציאציה של סקריפט ה-service worker, אחרת אתם עלולים להיתקל במצב הלא רצוי הזה:
- למשתמש יש אפליקציית אינטרנט עם IndexedDB (IDB) בגרסה N
- אפליקציית אינטרנט חדשה נדחפת עם גרסת IDB N+1
- משתמש נכנס לאפליקציית PWA, מה שמפעיל את ההורדה של עובד השירות החדש
- ה-service worker החדש קורא מ-IDB לפני שהוא רושם את פונקציית הטיפול באירוע
install
, ומפעיל מחזור שדרוג של IDB מ-N ל-N+1 - למשתמש יש לקוח ישן בגרסה N, תהליך השדרוג של Service Worker נתקע בזמן שהחיבורים הפעילים עדיין פתוחים לגרסה הישנה של מסד הנתונים.
- קובץ השירות (service worker) נתקע ולא מתקין את עצמו אף פעם
במקרה שלנו, המטמון בוטל בהתקנת ה-service worker, כך שאם ה-service worker לא הוטמע אף פעם, המשתמשים לא קיבלו את האפליקציה המעודכנת.
עמידות
סקריפטים של שירותי עבודה פועלים ברקע, אבל אפשר גם לסיים אותם בכל שלב, גם באמצע פעולות קלט/פלט (רשת, IDB וכו'). תהליך ארוך צריך להיות ניתן להמשך בכל שלב.
במקרה של תהליך סנכרון שהעלה קבצים גדולים לשרת ושמר אותם ב-IDB, הפתרון שלנו להעלאות חלקיות שהופסק הוא לנצל את המערכת לאפשרות המשך של ספריית ההעלאות הפנימית שלנו, לשמור את כתובת ה-URL של ההעלאה לאפשרות המשך ב-IDB לפני ההעלאה, ולהשתמש בכתובת ה-URL הזו כדי להמשיך את ההעלאה אם היא לא הושלמה בפעם הראשונה. בנוסף, לפני כל פעולת קלט/פלט ממושכת, המצב נשמר ב-IDB כדי לציין איפה היינו בתהליך לכל רשומה.
הימנעות מהסתמכות על מצב גלובלי
קובצי שירות נמצאים בהקשר שונה, ולכן סמלים רבים שציפיתם לראות לא מופיעים. חלק גדול מהקוד שלנו רץ גם בהקשר של window
וגם בהקשר של שירות העבודה (למשל רישום ביומן, דגלים, סנכרון וכו'). הקוד צריך להגן על השירותים שהוא משתמש בהם, כמו אחסון מקומי או קובצי Cookie. אפשר להשתמש ב-globalThis
כדי להפנות לאובייקט הגלובלי באופן שיעבוד בכל ההקשרים. כמו כן, מומלץ להשתמש בנתונים שמאוחסנים במשתנים גלובליים במשורה, כי אין ערובה למועד שבו הסקריפט יסתיים והמצב יוסר.
פיתוח מקומי
אחד מהרכיבים העיקריים של שירותי העבודה הוא שמירת משאבים במטמון באופן מקומי. עם זאת, במהלך הפיתוח זהו ההפך ממה שרוצים, במיוחד כשהעדכונים מתבצעים באופן עצל. עדיין צריך להתקין את ה-server worker כדי לנפות באגים בבעיות או לעבוד עם ממשקי API אחרים כמו סנכרון ברקע או התראות. ב-Chrome אפשר לעשות זאת באמצעות כלי הפיתוח ל-Chrome. לשם כך, מסמנים את התיבה Bypass for network (חלונית Application > חלונית Service workers) ומסמנים גם את התיבה Disable cache בחלונית Network כדי להשבית גם את מטמון הזיכרון. כדי לכסות יותר דפדפנים, בחרנו בפתרון אחר: הוספת דגל להשבתת האחסון במטמון ב-service worker שלנו, שמופעל כברירת מחדל בגרסאות build למפתחים. כך המפתחים תמיד מקבלים את השינויים האחרונים ללא בעיות שקשורות לאחסון במטמון. חשוב לכלול גם את הכותרת Cache-Control: no-cache
כדי למנוע מהדפדפן לשמור נכסים בזיכרון cache.
Lighthouse
ב-Lighthouse יש כמה כלים לניפוי באגים שמועילים לאפליקציות PWA. הכלי סורק אתר ויוצר דוחות שכוללים אפליקציות לנייד (PWA), ביצועים, נגישות, אופטימיזציה למנועי חיפוש ושיטות מומלצות אחרות. מומלץ להפעיל את Lighthouse בשילוב עם אינטגרציה רציפה כדי לקבל התראות אם אתם מפירים אחד מהקריטריונים להגדרה כ-PWA. זה קרה לנו פעם אחת, כשה-service worker לא הוטמע ולא שמנו לב לכך לפני ששלחנו את הגרסה לסביבת הייצור. אם היינו משתמשים ב-Lighthouse כחלק מ-CI, היינו יכולים למנוע את זה.
אימוץ פיתוח רציף (continuous delivery)
מכיוון ששירותי ה-Workers יכולים להתעדכן באופן אוטומטי, למשתמשים אין אפשרות להגביל שדרוגים. כך תוכלו לצמצם באופן משמעותי את מספר הלקוחות הלא מעודכנים בעולם. כשהמשתמש פתח את האפליקציה, ה-service worker הציג את הלקוח הישן בזמן שהוא הוריד את הלקוח החדש בזליגה. אחרי הורדת הלקוח החדש, המשתמש יתבקש לרענן את הדף כדי להשתמש בתכונות חדשות. גם אם המשתמש יתעלם מהבקשה הזו, בפעם הבאה שהוא ירענן את הדף, הוא יקבל את הגרסה החדשה של הלקוח. כתוצאה מכך, קשה למשתמש לדחות עדכונים באותה דרך שבה הוא יכול לדחות עדכונים באפליקציות ל-iOS או ל-Android.
הצלחנו להשיק שינויים משמעותיים בקצה העורפי עם זמן העברה קצר מאוד ללקוחות. בדרך כלל אנחנו נותנים למשתמשים חודש לעדכן את הלקוחות שלהם לגרסאות חדשות יותר לפני שאנחנו מבצעים שינויים משמעותיים. מכיוון שהאפליקציה הייתה לא פעילה, לקוחות ישנים יותר היו יכולים להמשיך להתקיים אם המשתמשים לא היו פותחים אותה במשך זמן רב. ב-iOS, שירותי העבודה מגורשים אחרי כמה שבועות, ולכן המצב הזה לא קורה. ב-Android, אפשר לצמצם את הבעיה הזו על ידי אי הצגת התוכן בזמן שהוא לא עדכני, או על ידי תאריך תפוגה ידני של התוכן אחרי כמה שבועות. בפועל, אף פעם לא נתקלנו בבעיות שקשורות ללקוחות לא מעודכנים. מידת הדרישה של כל צוות תלויה בתרחיש לדוגמה הספציפי שלו, אבל אפליקציות PWA מספקות גמישות רבה יותר בהשוואה לאפליקציות ל-iOS או ל-Android.
אחזור ערכים של קובצי cookie ב-service worker
לפעמים יש צורך לגשת לערכים של קובצי cookie בהקשר של Service Worker. במקרה שלנו, היינו צריכים לגשת לערכים של קובצי cookie כדי ליצור אסימון לאימות בקשות API מהדומיין הנוכחי. ב-service worker, ממשקי API סינכרוניים כמו document.cookies
לא זמינים. תמיד אפשר לשלוח הודעה מקובץ השירות (service worker) ללקוחות פעילים (עם חלונות) כדי לבקש את ערכי קובצי ה-Cookie. עם זאת, יכול להיות שה-Service Worker יפעל ברקע בלי לקוח עם חלון זמין, למשל במהלך סנכרון ברקע. כדי לעקוף את הבעיה הזו, יצרנו נקודת קצה בשרת הקצה הקדמי שלנו, שפשוט החזירה ללקוח את ערך קובץ ה-cookie. עובד השירות של ה-Service Worker שלח בקשת רשת לנקודת הקצה הזו וקרא את התשובה כדי לקבל את ערכי קובצי ה-cookie.
בעקבות הפרסום של Cookie Store API, העקיפה הזו כבר לא נחוצה לדפדפנים שתומכים בה, כי היא מספקת גישה אסינכרונית לקובצי cookie של הדפדפן ואפשר להשתמש בה ישירות על ידי Service Worker.
מלכודות לעובדי שירות שלא נוצרו
מוודאים שהסקריפט של ה-service worker משתנה אם קובץ סטטי כלשהו ששמור במטמון משתנה
דפוס נפוץ של אפליקציות PWA הוא ש-service worker מתקין את כל קובצי האפליקציה הסטטיים במהלך שלב install
, וכך מאפשר ללקוחות לגשת ישירות למטמון של Cache Storage API בכל הביקורים הבאים. קובצי שירות (service worker) מותקנים רק כשהדפדפן מזהה שהסקריפט של Service Worker השתנה באופן כלשהו, לכן נאלצנו לוודא שקובץ הסקריפט של Service Worker עצמו השתנה באופן כלשהו כשהקובץ ששמור במטמון השתנה. עשינו זאת באופן ידני על ידי הטמעת גיבוב של קבוצת הקבצים של המשאבים הסטטיים בסקריפט של ה-service worker, כך שכל גרסה יצרה קובץ JavaScript נפרד של ה-service worker. ספריות של קובצי שירות כמו Workbox מבצעות את התהליך הזה באופן אוטומטי.
בדיקות יחידה
ממשקי ה-API של שירותי העבודה פועלים על ידי הוספת מאזיני אירועים לאובייקט הגלובלי. לדוגמה:
self.addEventListener('fetch', (evt) => evt.respondWith(fetch('/foo')));
יכול להיות שיהיה קשה לבדוק את זה כי צריך ליצור תרמית של הטריגר של האירוע, של אובייקט האירוע, להמתין להפעלה החוזרת (callback) של respondWith()
ואז להמתין להבטחה, ולבסוף לבצע טענת נכוֹנוּת (assertion) על התוצאה. דרך קלה יותר לבנות את המבנה היא להעביר את כל ההטמעה לקובץ אחר, שקל יותר לבדוק אותו.
import fetchHandler from './fetch_handler.js';
self.addEventListener('fetch', (evt) => evt.respondWith(fetchHandler(evt)));
בגלל הקשיים בבדיקת יחידה של סקריפט של Service Worker, שמרנו את הסקריפט של קובץ הליבה של שירות הליבה ככל האפשר, ופיצלנו את רוב ההטמעה למודולים אחרים. מכיוון שהקבצים האלה היו רק מודולים רגילים של JS, היה קל יותר לבצע בדיקות יחידה שלהם באמצעות ספריות בדיקה רגילות.
כדאי להמשיך לעקוב אחרינו כדי לקבל פרטים על חלקים 2 ו-3
בחלקים 2 ו-3 של הסדרה הזו נדון בניהול מדיה ובבעיות ספציפיות ל-iOS. אם יש לכם שאלות נוספות לגבי פיתוח אפליקציות PWA ב-Google, תוכלו להיכנס לפרופיל המחבר שלנו כדי לברר איך ליצור איתנו קשר: