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

מה הצוות של Bulletin למד על קובצי שירות (service worker) במהלך הפיתוח של אפליקציית PWA.

Douglas Parker
Douglas Parker
Joel Riley
Joel Riley
Dikla Cohen
Dikla Cohen

זהו הפרק הראשון בסדרת פוסטים בבלוג על הלקחים שלמד צוות 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, אחרת אתם עלולים להיתקל במצב הלא רצוי הזה:

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

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

עמידות

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

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

הימנעות מהסתמכות על מצב גלובלי

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

פיתוח מקומי

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

עם השקת Cookie Store API, לא תצטרכו יותר להשתמש בפתרון החלופי הזה בדפדפנים שתומכים בו, כי הוא מספק גישה אסינכררונית לקובצי cookie בדפדפן, ואפשר להשתמש בו ישירות על ידי ה-service worker.

מלכודות בקובצי שירות (service workers) שלא נוצרו

מוודאים שהסקריפט של ה-service worker משתנה אם קובץ סטטי כלשהו ששמור במטמון משתנה

דפוס נפוץ של אפליקציות PWA הוא ש-service worker מתקין את כל קובצי האפליקציה הסטטיים במהלך שלב install, וכך מאפשר ללקוחות לגשת ישירות למטמון של Cache Storage API בכל הביקורים הבאים . שירותי העבודה מותקנים רק כשהדפדפן מזהה שהסקריפט של שירות העבודה השתנה בצורה כלשהי, לכן נאלצנו לוודא שקובץ הסקריפט של שירות העבודה השתנה בצורה כלשהי כשקובץ ששמור במטמון השתנה. עשינו זאת באופן ידני על ידי הטמעת גיבוב של קבוצת הקבצים של המשאבים הסטטיים בסקריפט של ה-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)));

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

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

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