בניית PWA ב-Google, חלק 1

מה צוות Bulletin למד על Service Workers במהלך פיתוח PWA.

דאגלס פרקר
דאגלס פרקר
ג'ואל ריילי
ג'ואל ריילי
דיקלה כהן
דיקלה כהן

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

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

רקע

'מבזק' היה בפיתוח פעיל מאמצע 2017 עד אמצע 2019.

למה בחרנו ליצור PWA

לפני שנתעמק בתהליך הפיתוח, נסביר למה בניית PWA הייתה אפשרות אטרקטיבית לפרויקט הזה:

  • יכולת לבצע חזרה במהירות. חשוב במיוחד כי Bulletin תשתתף בפיילוט בכמה שווקים.
  • Single code base (בסיס קוד יחיד). המשתמשים שלנו פוצלו פחות או יותר באופן שווה בין Android ל-iOS. המשמעות של PWA הייתה ליצור אפליקציית אינטרנט אחת שתעבוד בשתי הפלטפורמות. זה הגביר את המהירות ואת ההשפעה של הצוות.
  • מתעדכנים במהירות וללא קשר להתנהגות המשתמשים. אפליקציות PWA יכולות להתעדכן באופן אוטומטי, וכך להפחית את כמות הלקוחות הלא מעודכנים בטבע. הצלחנו לדחוף שינויים עורפיים שעלולים לגרום לכשלים, תוך פרק זמן קצר מאוד להעברת הנתונים עבור הלקוחות.
  • שילוב בקלות עם אפליקציות של צד ראשון ושל צד שלישי. שילובים כאלה היו בגדר דרישה לאפליקציה. PWA לעיתים קרובות כללה רק פתיחה של כתובת URL.
  • הסרת החיכוך של התקנת אפליקציה.

המסגרת שלנו

ב-Bulletin השתמשנו ב-Polymer, אבל כל framework מודרנית ונתמכת היטב תפעל.

מה למדנו על Service Workers

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

יוצרים אותו אם אפשר

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

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

לא כל הספריות תואמות ל-Service-worker

ספריות JS מסוימות מניחות הנחות שלא פועלות כצפוי כאשר הן מופעלות על ידי Service Worker. לצורך מכונה, בהנחה ש-window או document זמינים, או שמשתמשים ב-API שלא זמין ל-Service Workers (XMLHttpRequest, אחסון מקומי וכו'). חשוב לוודא שכל הספריות הקריטיות שנדרשות לאפליקציה תואמות ל-Service Worker. המטרה של ה-PWA הספציפית הזו הייתה להשתמש ב-gapi.js לצורך אימות, אבל לא יכולנו לעשות זאת כי היא לא תמכה ב-Service Workers. בנוסף, מחברי ספריות צריכים לצמצם או להסיר הנחות מיותרות לגבי ההקשר של JavaScript ככל האפשר כדי לתמוך במקרי שימוש של Service Worker, למשל על ידי הימנעות מממשקי API שלא תואמים ל-Service Worker והימנעות ממצב גלובלי.

הימנעות מגישה ל-IndexedDB במהלך האתחול

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

  1. למשתמש יש אפליקציית אינטרנט בגרסת IndexedDB (IDB) N
  2. אפליקציית אינטרנט חדשה נדחפת עם גרסת IDB N+1
  3. המשתמש נכנס ל-PWA, שמפעיל הורדה של קובץ שירות (service worker) חדש
  4. קובץ שירות (service worker) חדש קורא מ-IDB לפני רישום הגורם המטפל באירועים של install, וגורם למחזור של שדרוג IDB לעבור מ-N ל-N+1.
  5. מכיוון שלמשתמש יש לקוח ישן בגרסה N, תהליך השדרוג של קובץ השירות (service worker) נתקע כשחיבורים פעילים עדיין פתוחים לגרסה הישנה של מסד הנתונים
  6. קובץ שירות (service worker) נתקע ואף פעם לא מתקין

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

עמידות

סקריפטים של Service Worker פועלים ברקע, אבל הם עלולים גם להסתיים בכל שלב, גם באמצע הפעולות של קלט/פלט (I/O) (רשת, IDB וכו'). צריך להמשיך כל תהליך ממושך בכל שלב.

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

לא תלויים במצב הגלובלי

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

פיתוח מקומי

רכיב עיקרי של קובצי שירות (service worker) הוא שמירת משאבים באופן מקומי במטמון. עם זאת, במהלך הפיתוח זה בדיוק ההיפך הרצוי, במיוחד כשהעדכונים מתבצעים בהדרגה. אתם עדיין רוצים שה-server worker יותקן כדי שתוכלו לנפות באגים בו או לעבוד עם ממשקי API אחרים כמו סנכרון ברקע או התראות. ב-Chrome אפשר לעשות זאת באמצעות כלי הפיתוח ל-Chrome על ידי הפעלת תיבת הסימון עקיפה לרשת (החלונית Application > החלונית Service workers) בנוסף להפעלת תיבת הסימון השבתת המטמון בחלונית רשת, כדי להשבית גם את מטמון הזיכרון. כדי לכסות דפדפנים נוספים, בחרנו בפתרון אחר על ידי הוספת דגל להשבתה של השמירה במטמון ב-Service Worker, שמופעל כברירת מחדל ב-builders של מפתחים. כך המפתחים תמיד יקבלו את השינויים האחרונים שלהם ללא בעיות של שמירה במטמון. חשוב לכלול גם את הכותרת Cache-Control: no-cache כדי למנוע מהדפדפן לשמור נכסים במטמון.

מגדלור

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

שימוש במסירה רציפה

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

הצלחנו לדחוף שינויים עורפיים לשיבושים תוך פרק זמן קצר מאוד להעברת הנתונים של הלקוחות. בדרך כלל אנחנו נותנים למשתמשים חודש כדי להתעדכן בלקוחות חדשים יותר לפני שהם מבצעים שינויים שעלולים לגרום לכשלים. מכיוון שהאפליקציה הייתה לא פעילה, בפועל לקוחות ישנים היו יכולים להתקיים בטבע אם המשתמש לא פתח את האפליקציה במשך זמן רב. ב-iOS, ה-Service Workers מרוקנים אחרי כמה שבועות כך שמקרה כזה לא יקרה. ב-Android, אפשר לצמצם את הבעיה על ידי אי-הצגת התוכן בזמן שהוא לא פעיל או שתוקף התוכן יפוג באופן ידני אחרי מספר שבועות. בפועל, אף פעם לא נתקלנו בבעיות של לקוחות לא פעילים. עד כמה צוות נתון רוצה להיות בקפדנות כאן תלוי בתרחיש השימוש הספציפי שלו, אבל אפליקציות PWA מספקות גמישות רבה יותר באופן משמעותי מאפליקציות ל-iOS או ל-Android.

קבלת ערכים של קובצי cookie ב-Service Worker

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

בעקבות ההשקה של Cookie Store API, לא יהיה עוד צורך לעקוף את הבעיה הזו בדפדפנים שתומכים בו, כי הוא מספק גישה אסינכרונית לקובצי cookie של הדפדפן ויכול לשמש ישירות את ה-Service Worker.

נקודות חסינות ל-Service Workers שלא נוצרו

ודאו שהסקריפט של Service Worker משתנה אם חל שינוי כלשהו בקובץ סטטי סטטי שנשמר במטמון

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

בדיקת יחידה

ממשקי Service Worker API פועלים על ידי הוספת פונקציות event listener לאובייקט הגלובלי. לדוגמה:

self.addEventListener('fetch', (evt) => evt.respondWith(fetch('/foo')));

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

import fetchHandler from './fetch_handler.js';
self.addEventListener('fetch', (evt) => evt.respondWith(fetchHandler(evt)));

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

המשיכו להתעדכן בחלקים 2 ו-3

בחלקים 2 ו-3 של סדרה זו נדבר על ניהול מדיה ובעיות ספציפיות ל-iOS. אם תרצו לשאול אותנו עוד על בניית PWA ב-Google, היכנסו לפרופילים של המחברים כדי לגלות איך ליצור איתנו קשר: