פיתוח של Progressive Web Apps 02.0: מדריך למתחילים בנושא מצב אופליין

ה-codelab הזה הוא חלק מקורס ההדרכה בנושא פיתוח Progressive Web Apps, שפותח על ידי צוות ההדרכה של Google Developers. כדי להפיק את המרב מהקורס הזה, מומלץ לעבוד על ה-codelabs לפי הסדר.

פרטים מלאים על הקורס זמינים במאמר סקירה כללית על פיתוח Progressive Web Apps.

מבוא

בשיעור ה-Lab הזה נשתמש ב-Lighthouse כדי לבדוק אתר אינטרנט לפי תקני Progressive Web App ‏ (PWA). בנוסף, תוכלו להוסיף פונקציונליות אופליין באמצעות Service Worker API.

מה תלמדו

  • איך מבצעים ביקורת באתרים באמצעות Lighthouse
  • איך מוסיפים אפשרות להשתמש באפליקציה במצב אופליין

מה חשוב לדעת

  • HTML,‏ CSS ו-JavaScript בסיסיים
  • היכרות עם Promises ב-ES2015

מה צריך

  • מחשב עם גישה לטרמינל או למעטפת
  • חיבור לאינטרנט
  • דפדפן Chrome (לשימוש ב-Lighthouse)
  • כלי לעריכת טקסט
  • אופציונלי: Chrome במכשיר Android

מורידים או משכפלים את מאגר pwa-training-labs מ-GitHub ומתקינים את גרסת ה-LTS של Node.js, אם צריך.

מנווטים לספרייה offline-quickstart-lab/app/ ומפעילים שרת פיתוח מקומי:

cd offline-quickstart-lab/app
npm install
node server.js

אפשר להפסיק את השרת בכל שלב באמצעות Ctrl-c.

פותחים את הדפדפן ועוברים אל localhost:8081/. אתם אמורים לראות שהאתר הוא דף אינטרנט סטטי ופשוט.

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

פותחים את התיקייה offline-quickstart-lab/app/ בכלי לעריכת טקסט. בתיקייה app/ תבנו את המעבדה.

התיקייה הזו מכילה:

  • התיקייה images/ מכילה תמונות לדוגמה
  • styles/main.css הוא גיליון הסגנונות הראשי
  • index.html הוא דף ה-HTML הראשי של האתר לדוגמה
  • package-lock.json ו-package.json עוקבים אחרי יחסי התלות של האפליקציה (יחסי התלות היחידים במקרה הזה הם של שרת הפיתוח המקומי)
  • server.js הוא שרת פיתוח מקומי לבדיקה
  • service-worker.js הוא קובץ ה-service worker (כרגע ריק)

לפני שמתחילים לבצע שינויים באתר, כדאי לבצע ביקורת באמצעות Lighthouse כדי לראות מה אפשר לשפר.

חוזרים לאפליקציה (ב-Chrome) ופותחים את הכרטיסייה ביקורות בכלים למפתחים. אמורים להופיע סמל Lighthouse ואפשרויות ההגדרה. בוחרים באפשרות 'נייד' בשדה מכשיר, בוחרים בכל הביקורות, בוחרים באחת מאפשרויות ההגבלה ובוחרים באפשרות ניקוי האחסון:

לוחצים על הרצת בדיקות. הביקורות נמשכות כמה רגעים.

הסבר

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

הערה: הניקוד ב-Lighthouse הוא קירוב, ויכולים להיות גורמים בסביבה שלכם שישפיעו עליו (לדוגמה, אם פתוחים הרבה חלונות בדפדפן). יכול להיות שהציונים שלכם לא יהיו בדיוק כמו אלה שמוצגים כאן.

המקטע Progressive Web App אמור להיראות כך:

הדוח כולל ציונים ומדדים בחמש קטגוריות:

  • Progressive Web App
  • ביצועים
  • נגישות
  • שיטות מומלצות
  • אופטימיזציה עבור מנועי חיפוש

כפי שאפשר לראות, האפליקציה שלנו קיבלה ציון נמוך בקטגוריה Progressive Web App ‏ (PWA). בואו נשפר את הציון שלנו!

כדאי להקדיש רגע כדי לעיין בקטע PWA בדוח ולראות מה חסר.

רישום קובץ שירות (service worker)

אחת מהשגיאות שמפורטות בדוח היא שלא נרשם קובץ שירות (service worker). כרגע יש לנו קובץ ריק של Service Worker בכתובת app/service-worker.js.

מוסיפים את הסקריפט הבא לתחתית הקובץ index.html, ממש לפני תג הסגירה </body>:

<script>
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('service-worker.js')
      .then(reg => {
        console.log('Service worker registered! 😎', reg);
      })
      .catch(err => {
        console.log('😥 Service worker registration failed: ', err);
      });
  });
}
</script>

הסבר

הקוד הזה רושם את קובץ השירות (service worker) הריק service-worker.js אחרי שהדף נטען. עם זאת, קובץ ה-service worker הנוכחי ריק ולא יעשה דבר. בשלב הבא נוסיף קוד שירות.

שמירת משאבים במטמון מראש

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

מוסיפים את הקוד הבא לקובץ service worker‏ (service-worker.js):

const cacheName = 'cache-v1';
const precacheResources = [
  '/',
  'index.html',
  'styles/main.css',
  'images/space1.jpg',
  'images/space2.jpg',
  'images/space3.jpg'
];

self.addEventListener('install', event => {
  console.log('Service worker install event!');
  event.waitUntil(
    caches.open(cacheName)
      .then(cache => {
        return cache.addAll(precacheResources);
      })
  );
});

self.addEventListener('activate', event => {
  console.log('Service worker activate event!');
});

self.addEventListener('fetch', event => {
  console.log('Fetch intercepted for:', event.request.url);
  event.respondWith(caches.match(event.request)
    .then(cachedResponse => {
        if (cachedResponse) {
          return cachedResponse;
        }
        return fetch(event.request);
      })
    );
});

עכשיו חוזרים לדפדפן ומרעננים את האתר. בודקים במסוף ש-service worker:

  • רשום
  • מותקנת
  • הופעל

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

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

הערה: יכול להיות שתופיע שגיאה בקונסולה שמציינת שלא הייתה אפשרות לאחזר את ה-service worker: ‏ An unknown error occurred when fetching the script. service-worker.js Failed to load resource: net::ERR_CONNECTION_REFUSED. השגיאה הזו מוצגת כי הדפדפן לא הצליח לאחזר את סקריפט ה-service worker (כי האתר במצב אופליין), אבל זה צפוי כי אי אפשר להשתמש ב-service worker כדי לשמור את עצמו במטמון. אחרת, דפדפן המשתמש יישאר תקוע עם אותו Service Worker לנצח.

הסבר

אחרי שקובץ השירות נרשם על ידי סקריפט הרישום ב-index.html, מתרחש האירוע install של קובץ השירות. במהלך האירוע הזה, ה-install event listener פותח מטמון עם שם ומאחסן במטמון את הקבצים שצוינו באמצעות השיטה cache.addAll. התהליך הזה נקרא 'טעינה מראש' כי הוא מתרחש במהלך האירוע install, שבדרך כלל מתרחש בפעם הראשונה שמשתמש מבקר באתר.

אחרי שקובץ שירות מותקן, ואם קובץ שירות אחר לא שולט כרגע בדף, קובץ השירות החדש 'מופעל' (המאזין לאירוע activate מופעל בקובץ השירות) והוא מתחיל לשלוט בדף.

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

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

הערה: האירוע activate לא משמש לשום דבר חוץ מרישום בכניסה בדוגמה הזו. האירוע הזה נכלל כדי לעזור בפתרון בעיות במחזור החיים של Service Worker.

אופציונלי: אפשר לראות את המשאבים שנשמרו במטמון גם בכרטיסייה Application (אפליקציה) ב'כלים למפתחים'. לשם כך, מרחיבים את הקטע Cache Storage (אחסון במטמון):

מפעילים מחדש את שרת הפיתוח באמצעות node server.js ומרעננים את האתר. לאחר מכן פותחים שוב את הכרטיסייה Audits (ביקורות) ב'כלים למפתחים' ומריצים מחדש את הביקורת של Lighthouse על ידי בחירה באפשרות New Audit (הסימן פלוס בפינה הימנית העליונה). בסיום הביקורת, תראו שציון ה-PWA שלנו השתפר משמעותית, אבל עדיין אפשר לשפר אותו. נמשיך לשפר את הציון שלנו בקטע הבא.

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

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

יצירת קובץ מניפסט

יוצרים קובץ ב-app/ בשם manifest.json ומוסיפים את הקוד הבא:

{
  "name": "Space Missions",
  "short_name": "Space Missions",
  "lang": "en-US",
  "start_url": "/index.html",
  "display": "standalone",
  "theme_color": "#FF9800",
  "background_color": "#FF9800",
  "icons": [
    {
      "src": "images/touch/icon-128x128.png",
      "sizes": "128x128"
    },
    {
      "src": "images/touch/icon-192x192.png",
      "sizes": "192x192"
    },
    {
      "src": "images/touch/icon-256x256.png",
      "sizes": "256x256"
    },
    {
      "src": "images/touch/icon-384x384.png",
      "sizes": "384x384"
    },
    {
      "src": "images/touch/icon-512x512.png",
      "sizes": "512x512"
    }
  ]
}

התמונות שמוזכרות בקובץ המניפסט כבר מסופקות באפליקציה.

לאחר מכן מוסיפים את קוד ה-HTML הבא לתחתית התג <head> ב-index.html:

<link rel="manifest" href="manifest.json">

<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="application-name" content="Space Missions">
<meta name="apple-mobile-web-app-title" content="Space Missions">
<meta name="theme-color" content="#FF9800">
<meta name="msapplication-navbutton-color" content="#FF9800">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="msapplication-starturl" content="/index.html">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<link rel="icon" sizes="128x128" href="/images/touch/icon-128x128.png">
<link rel="apple-touch-icon" sizes="128x128" href="/images/touch/icon-128x128.png">
<link rel="icon" sizes="192x192" href="icon-192x192.png">
<link rel="apple-touch-icon" sizes="192x192" href="/images/touch/icon-192x192.png">
<link rel="icon" sizes="256x256" href="/images/touch/icon-256x256.png">
<link rel="apple-touch-icon" sizes="256x256" href="/images/touch/icon-256x256.png">
<link rel="icon" sizes="384x384" href="/images/touch/icon-384x384.png">
<link rel="apple-touch-icon" sizes="384x384" href="/images/touch/icon-384x384.png">
<link rel="icon" sizes="512x512" href="/images/touch/icon-512x512.png">
<link rel="apple-touch-icon" sizes="512x512" href="/images/touch/icon-512x512.png">

חוזרים לאתר. בכרטיסייה Application (אפליקציה) של הכלים למפתחים, בוחרים בקטע Clear storage (ניקוי האחסון) ולוחצים על Clear site data (ניקוי נתוני האתר). לאחר מכן מרעננים את הדף. עכשיו בוחרים בקטע Manifest (מניפסט). אמורים להופיע הסמלים ואפשרויות ההגדרה שמוגדרים בקובץ manifest.json. אם השינויים לא מופיעים, פותחים את האתר בחלון אנונימי ובודקים שוב.

הסבר

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

בזמן כתיבת המאמר הזה, התמיכה בדפדפנים מסוימים עדיין בפיתוח, ותגי <meta> מגדירים קבוצת משנה של התכונות האלה בדפדפנים מסוימים שעדיין אין בהם תמיכה מלאה.

היינו צריכים לנקות את נתוני האתר כדי להסיר את הגרסה הישנה של index.html ששמורה במטמון (כי בגרסה הזו לא היה קישור למניפסט). כדאי להריץ עוד ביקורת של Lighthouse ולראות כמה השתפר הציון של ה-PWA.

הפעלת ההצעה להתקנה

השלב הבא בהתקנת האפליקציה שלנו הוא הצגת ההודעה למשתמשים. ב-Chrome 67, המשתמשים קיבלו הנחיה אוטומטית, אבל החל מ-Chrome 68, ההנחיה להתקנה מופעלת באופן פרוגרמטי בתגובה למחוות משתמש.

מוסיפים לחלק העליון של index.html (אחרי התג <main>) לחצן ורצועת באנר עם הכיתוב 'התקנת האפליקציה' באמצעות הקוד הבא:

<section id="installBanner" class="banner">
    <button id="installBtn">Install app</button>
</section>

לאחר מכן, מעצבים את הבאנר על ידי הוספת הסגנונות הבאים ל-styles/main.css:

.banner {
  align-content: center;
  display: none;
  justify-content: center;
  width: 100%;
}

שומרים את הקובץ. לבסוף, מוסיפים את תג הסקריפט הבא אל index.html:

  <script>
    let deferredPrompt;
    window.addEventListener('beforeinstallprompt', event => {

      // Prevent Chrome 67 and earlier from automatically showing the prompt
      event.preventDefault();

      // Stash the event so it can be triggered later.
      deferredPrompt = event;

      // Attach the install prompt to a user gesture
      document.querySelector('#installBtn').addEventListener('click', event => {

        // Show the prompt
        deferredPrompt.prompt();

        // Wait for the user to respond to the prompt
        deferredPrompt.userChoice
          .then((choiceResult) => {
            if (choiceResult.outcome === 'accepted') {
              console.log('User accepted the A2HS prompt');
            } else {
              console.log('User dismissed the A2HS prompt');
            }
            deferredPrompt = null;
          });
      });

      // Update UI notify the user they can add to home screen
      document.querySelector('#installBanner').style.display = 'flex';
    });
  </script>

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

הסבר

קוד ה-HTML וה-CSS מוסיף באנר ולחצן מוסתרים שאפשר להשתמש בהם כדי לאפשר למשתמשים להפעיל את בקשת ההתקנה.

אחרי שהאירוע beforeinstallprompt מופעל, אנחנו מונעים את חוויית ברירת המחדל (שבה Chrome מגרסה 67 ומגרסאות קודמות מציג אוטומטית למשתמשים הנחיה להתקין) ומתעדים את beforeinstallevent במשתנה הגלובלי deferredPrompt. לאחר מכן, לחצן 'התקנת האפליקציה' מוגדר להצגת ההנחיה באמצעות השיטה של beforeinstallevent prompt(). אחרי שהמשתמש בוחר (להתקין או לא), ההבטחה userChoice נפתרת עם הבחירה של המשתמש (outcome). לבסוף, אנחנו מציגים את לחצן ההתקנה אחרי שהכול מוכן.

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

מקורות מידע נוספים

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

כדי לראות את כל ה-codelabs בקורס ההדרכה בנושא PWA, אפשר לעיין ב-codelab המבוא לקורס.