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

מה הצוות של Bulletin למד על עובדי שירות בזמן הפיתוח של PWA.

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

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

בפוסט הראשון הזה נדון קודם בקצת מידע ברקע ולאחר מכן נתעמק בכל הדברים שלמדנו על קובצי שירות (service worker).

רקע

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

למה בחרנו לפתח PWA

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

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

המסגרת שלנו

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

מה למדנו על קובצי שירות (service worker)

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

כדאי ליצור אותו, אם אפשר

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

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

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

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

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

לא מומלץ לקרוא את IndexedDB כשמאתחלים את הסקריפט של Service Worker, כי מצב אחר עלול להוביל למצב הלא רצוי הבא:

  1. למשתמש יש אפליקציית אינטרנט עם גרסה N של IndexedDB (IDB)
  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 worker) קיימים בהקשר שונה, ולכן לא מופיעים סמלים רבים. חלק גדול מהקוד שלנו פעל גם בהקשר של window וגם בהקשר של Service Worker (כמו רישום ביומן, דגלים, סנכרון וכו'). הקוד צריך להגן על השירותים שהוא משתמש בהם, כמו אחסון מקומי או קובצי Cookie. אפשר להשתמש ב-globalThis כדי להפנות לאובייקט הגלובלי באופן שיפעל בכל ההקשרים. כדאי גם להשתמש בכמות מצומצמת של נתונים שמאוחסנים במשתנים גלובליים, כי אי אפשר להבטיח מתי הסקריפט יסתיים והמצב יוסר.

פיתוח מקומי

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

Lighthouse

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

שימוש בהצגה רציפה (CD)

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

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

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

בדיקות יחידה (unit testing)

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

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

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

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

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

בקרוב יופיעו חלקים 2 ו-3

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