יצירת שרת התראות

בשיעור ה-Codelab הזה תבנה שרת של התראות Push. השרת ינהל רשימה של מינויי Push וישלח אליהם התראות.

קוד הלקוח כבר הושלם – ב-Codelab הזה תלמדו על הפונקציונליות בצד השרת.

יצירת רמיקס של האפליקציה לדוגמה והצגתה בכרטיסייה חדשה

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

  1. לוחצים על רמיקס לעריכה כדי שיהיה אפשר לערוך את הפרויקט.
  2. כדי לראות תצוגה מקדימה של האתר, מקישים על View App ואז על Fullscreen מסך מלא.

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

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

היכרות עם האפליקציה ההתחלתית והקוד שלה

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

בכרטיסייה החדשה של Chrome:

  1. לוחצים על 'Control+Shift+J' (או 'Command+Option+J' ב-Mac) כדי לפתוח את כלי הפיתוח. לוחצים על הכרטיסייה מסוף.

  2. כדאי לנסות ללחוץ על לחצנים בממשק המשתמש (צריך לבדוק את הפלט במסוף Chrome dev).

    • רישום Service Worker רושם קובץ שירות (service worker) להיקף כתובת ה-URL של פרויקט Glitch. ביטול הרישום של קובץ השירות (service worker) מסיר אותו. אם מצורף אליו מינוי Push, המינוי מושבת גם הוא.

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

    • יש לך מינוי פעיל ב-Push, השירות יידע את המינוי הנוכחי מבקש מהשרת לשלוח התראה לנקודת הקצה שלו.

    • שלח התראה לכל המינויים מורה לשרת לשלוח התראה לכל נקודות הקצה של המינויים במסד הנתונים שלו.

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

בואו נראה מה קורה בצד השרת. כדי לראות הודעות מקוד השרת, מעיינים ביומן של Node.js בממשק של Glitch.

  • באפליקציית Glitch, לוחצים על כלים -> יומנים.

    סביר להניח שתוצג לך הודעה כמו Listening on port 3000.

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

    TODO: Implement sendNotifications()
    Endpoints to send to:  []
    

עכשיו נבחן קוד.

  • public/index.js מכיל את קוד הלקוח שהושלם. היא מבצעת זיהוי של תכונות, רושמת את קובץ השירות (service worker) ומבטלת את הרישום שלו, ושולטת במינוי של המשתמש להתראות דחיפה. הוא גם שולח לשרת מידע על מינויים חדשים ומינויים שנמחקו.

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

  • public/service-worker.js הוא קובץ שירות (service worker) פשוט שמתעד אירועים ומציגים התראות.

  • /views/index.html מכיל את ממשק המשתמש של האפליקציה.

  • השדה .env מכיל את משתני הסביבה שגליץ' טוענת לשרת האפליקציות כשהוא מופעל. המערכת תאכלס את .env בפרטי אימות לצורך שליחת התראות.

  • server.js הוא הקובץ שעבורו תבצעו את רוב העבודה שלכם במהלך ה-Codelab הזה.

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

    ב-Codelab הזה, תבצעו על הפריטים האלה מסוג TODO בזה אחר זה.

יצירה וטעינה של פרטי VAPID

פריט ה-TODO הראשון שלכם הוא ליצור פרטי VAPID, להוסיף אותם למשתני הסביבה של Node.js ולעדכן את קוד הלקוח והשרת בערכים החדשים.

רקע

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

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

באפליקציה הזו תשתמשו בחבילת npm של דחיפה באינטרנט כדי ליצור מפתחות VAPID וכדי להצפין ולשלוח התראות.

הטמעה

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

  1. משתמשים בפונקציה generateVAPIDKeys שבספרייה web-push כדי ליצור זוג מפתחות VAPID.

    ב-server.js, מסירים את התגובות סביב שורות הקוד הבאות:

    server.js

    // Generate VAPID keys (only do this once).
    /*
     * const vapidKeys = webpush.generateVAPIDKeys();
     * console.log(vapidKeys);
     */
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    
  2. אחרי שאפליקציית Glitch מפעילה מחדש את האפליקציה, היא יוצרת פלט של המפתחות שנוצרו ליומן Node.js בממשק של Glitch (לא למסוף Chrome). כדי לראות את מפתחות VAPID, בוחרים באפשרות כלים -> יומנים בממשק של Glitch.

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

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

  3. בקובץ .env, מעתיקים את מפתחות ה-VAPID ומדביקים אותם. יש לתחום את המפתחות במירכאות כפולות ("...").

    עבור VAPID_SUBJECT, אפשר להזין "mailto:test@test.test".

    .env

    # process.env.SECRET
    VAPID_PUBLIC_KEY=
    VAPID_PRIVATE_KEY=
    VAPID_SUBJECT=
    VAPID_PUBLIC_KEY="BN3tWzHp3L3rBh03lGLlLlsq..."
    VAPID_PRIVATE_KEY="I_lM7JMIXRhOk6HN..."
    VAPID_SUBJECT="mailto:test@test.test"
    
  4. ב-server.js, מוסיפים הערות לשתי שורות הקוד האלה שוב, מכיוון שצריך ליצור מפתחות VAPID רק פעם אחת.

    server.js

    // Generate VAPID keys (only do this once).
    /*
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    */
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    
  5. ב-server.js, טוענים את פרטי ה-VAPID ממשתני הסביבה.

    server.js

    const vapidDetails = {
      // TODO: Load VAPID details from environment variables.
      publicKey: process.env.VAPID_PUBLIC_KEY,
      privateKey: process.env.VAPID_PRIVATE_KEY,
      subject: process.env.VAPID_SUBJECT
    }
    
  6. מעתיקים את המפתח public ומדביקים אותו גם בקוד הלקוח.

    ב-public/index.js, מזינים את אותו ערך עבור VAPID_PUBLIC_KEY שהעתקתם לקובץ ה- .env:

    public/index.js

    // Copy from .env
    const VAPID_PUBLIC_KEY = '';
    const VAPID_PUBLIC_KEY = 'BN3tWzHp3L3rBh03lGLlLlsq...';
    ````
    

הטמעת פונקציונליות לשליחת התראות

רקע

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

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

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

ב-codelab זה נשתמש רק בשתי אפשרויות, המוגדרות באמצעות שורות הקוד הבאות:

let options = {
  TTL: 10000; // Time-to-live. Notifications expire after this.
  vapidDetails: vapidDetails; // VAPID keys from .env
};

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

האפשרות vapidDetails מכילה את מפתחות VAPID שטענת ממשתני הסביבה.

הטמעה

ב-server.js, משנים את הפונקציה sendNotifications באופן הבא:

server.js

function sendNotifications(database, endpoints) {
  // TODO: Implement functionality to send notifications.
  console.log('TODO: Implement sendNotifications()');
  console.log('Endpoints to send to: ', endpoints);
  let notification = JSON.stringify(createNotification());
  let options = {
    TTL: 10000, // Time-to-live. Notifications expire after this.
    vapidDetails: vapidDetails // VAPID keys from .env
  };
  endpoints.map(endpoint => {
    let subscription = database[endpoint];
    webpush.sendNotification(subscription, notification, options);
  });
}

מכיוון ש-webpush.sendNotification() מחזיר הבטחה, אפשר להוסיף בקלות טיפול בשגיאות.

ב-server.js, שנה שוב את הפונקציה sendNotifications:

server.js

function sendNotifications(database, endpoints) {
  let notification = JSON.stringify(createNotification());
  let options = {
    TTL: 10000; // Time-to-live. Notifications expire after this.
    vapidDetails: vapidDetails; // VAPID keys from .env
  };
  endpoints.map(endpoint => {
    let subscription = database[endpoint];
    webpush.sendNotification(subscription, notification, options);
    let id = endpoint.substr((endpoint.length - 8), endpoint.length);
    webpush.sendNotification(subscription, notification, options)
    .then(result => {
      console.log(`Endpoint ID: ${id}`);
      console.log(`Result: ${result.statusCode} `);
    })
    .catch(error => {
      console.log(`Endpoint ID: ${id}`);
      console.log(`Error: ${error.body} `);
    });
  });
}

טיפול במינויים חדשים

רקע

זה מה שקורה כשמשתמש נרשם לקבלת התראות:

  1. המשתמש לוחץ על הרשמה לדחיפה.

  2. הלקוח משתמש בקבוע VAPID_PUBLIC_KEY (מפתח VAPID הציבורי של השרת) כדי ליצור אובייקט subscription ייחודי וספציפי לשרת. האובייקט subscription נראה כך:

       {
         "endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9...",
         "expirationTime": null,
         "keys":
         {
           "p256dh": "BNYDjQL9d5PSoeBurHy2e4d4GY0sGJXBN...",
           "auth": "0IyyvUGNJ9RxJc83poo3bA"
         }
       }
    
  3. הלקוח שולח בקשת POST לכתובת ה-URL /add-subscription, כולל המינוי בפורמט JSON עם מחרוזת בגוף ההודעה.

  4. השרת מאחזר את המחרוזת subscription מהגוף של בקשת ה-POST, מנתח אותה בחזרה ל-JSON ומוסיף אותה למסד הנתונים של המינויים.

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

    {
      "https://fcm...1234": {
        endpoint: "https://fcm...1234",
        expirationTime: ...,
        keys: { ... }
      },
      "https://fcm...abcd": {
        endpoint: "https://fcm...abcd",
        expirationTime: ...,
        keys: { ... }
      },
      "https://fcm...zxcv": {
        endpoint: "https://fcm...zxcv",
        expirationTime: ...,
        keys: { ... }
      },
    }

עכשיו המינוי החדש זמין לשרת לשליחת התראות.

הטמעה

בקשות למינויים חדשים מגיעות למסלול /add-subscription, שהוא כתובת URL מסוג POST. תראה handler של מסלול מעקב ב-server.js:

server.js

app.post('/add-subscription', (request, response) => {
  // TODO: implement handler for /add-subscription
  console.log('TODO: Implement handler for /add-subscription');
  console.log('Request body: ', request.body);
  response.sendStatus(200);
});

בהטמעה, ה-handler הזה חייב:

  • מאחזרים את המינוי החדש מגוף הבקשה.
  • גישה למסד הנתונים של המינויים הפעילים.
  • הוסף את המינוי החדש לרשימת המינויים הפעילים.

כדי לטפל במינויים חדשים:

  • ב-server.js, משנים את ה-handler של המסלול עבור /add-subscription באופן הבא:

    server.js

    app.post('/add-subscription', (request, response) => {
      // TODO: implement handler for /add-subscription
      console.log('TODO: Implement handler for /add-subscription');
      console.log('Request body: ', request.body);
      let subscriptions = Object.assign({}, request.session.subscriptions);
      subscriptions[request.body.endpoint] = request.body;
      request.session.subscriptions = subscriptions;
      response.sendStatus(200);
    });

טיפול בביטולים של מינויים

רקע

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

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

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

הטמעה

בקשות לביטול מינויים מגיעות לכתובת ה-URL של ה-POST /remove-subscription.

ה-handler של נתיב ה-stub ב-server.js נראה כך:

server.js

app.post('/remove-subscription', (request, response) => {
  // TODO: implement handler for /remove-subscription
  console.log('TODO: Implement handler for /remove-subscription');
  console.log('Request body: ', request.body);
  response.sendStatus(200);
});

בהטמעה, ה-handler הזה חייב:

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

הגוף של בקשת ה-POST מהלקוח מכיל את נקודת הקצה שצריך להסיר:

{
  "endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9..."
}

כדי לטפל בביטולי מינויים:

  • ב-server.js, משנים את ה-handler של המסלול עבור /remove-subscription באופן הבא:

    server.js

  app.post('/remove-subscription', (request, response) => {
    // TODO: implement handler for /remove-subscription
    console.log('TODO: Implement handler for /remove-subscription');
    console.log('Request body: ', request.body);
    let subscriptions = Object.assign({}, request.session.subscriptions);
    delete subscriptions[request.body.endpoint];
    request.session.subscriptions = subscriptions;
    response.sendStatus(200);
  });