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

המסגרת שלנו

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

מה למדנו על קובצי שירות

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

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

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

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

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

בספריות JS מסוימות יש הנחות שלא פועלות כצפוי כשהן מופעלות על ידי שירות עבודה. לדוגמה, בהנחה ש-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, מה שמפעיל את ההורדה של עובד השירות החדש
  4. קובץ שירות (service worker) חדש קורא מ-IDB לפני רישום הגורם המטפל באירועים של install, וכתוצאה מכך נוצר מחזור שדרוג IDB מ-N ל-N+1
  5. מכיוון שלמשתמש יש לקוח ישן בגרסה N, תהליך השדרוג של ה-service worker נתקע כי החיבורים הפעילים עדיין פתוחים לגרסה הישנה של מסד הנתונים
  6. קובץ השירות (service worker) נתקע ולא מתקין את עצמו אף פעם

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

עמידות

למרות שסקריפטים של קובצי שירות פועלים ברקע, אפשר להפסיק אותם בכל שלב, גם במהלך פעולות קלט/פלט (I/O) (רשת, 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 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)));

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

כדאי להמשיך לעקוב אחרינו כדי לקבל פרטים על חלקים 2 ו-3

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