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

ב-codelab הזה תלמדו איך ליצור שרת התראות דחיפה. השרת ינהל רשימה של מינויים לקבלת התראות וישלח אליהן התראות.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • באפליקציית Glitch, לוחצים על Tools -> Logs.

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

    אם ניסיתם ללחוץ על Notify current subscription (עדכון המינויים הנוכחיים) או על Notify all subscriptions (עדכון כל המינויים) בממשק המשתמש של האפליקציה הפעילה, תוצג לכם גם ההודעה הבאה:

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

עכשיו נסתכל על קוד.

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

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

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

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

  • .env מכיל את משתני הסביבה ש-Glitch טוען לשרת האפליקציות בזמן ההפעלה. צריך לאכלס את השדה .env בפרטי אימות לשליחת התראות.

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

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

    ב-codelab הזה נעבד על כל אחד מהפריטים ברשימה הזו.

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

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

רקע

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

הפרוטוקול שמבטיח את האבטחה והפרטיות של התראות ה-push נקרא Voluntary Application Server Identification for Web Push‏ (VAPID). ב-VAPID נעשה שימוש בקריפטוגרפיה של מפתח ציבורי כדי לאמת את הזהות של אפליקציות, שרתי צד לקוח ונקודות קצה של מינויים, ולהצפין את תוכן ההתראות.

באפליקציה הזו תשתמשו בחבילת ה-npm של web-push כדי ליצור מפתחות VAPID, להצפין ולשלוח התראות.

הטמעה

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

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

    ב-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, בוחרים באפשרות Tools -> Logs בממשק של Glitch.

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

    אפליקציית 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/index.js, מזינים את אותו ערך בשביל VAPID_PUBLIC_KEY שהעתקת לקובץ ה- .env:

    public/index.js

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

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

רקע

באפליקציה הזו, תשתמשו בחבילת ה-npm של web-push כדי לשלוח התראות.

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

הפרמטר web-push מקבל כמה אפשרויות להתראות - לדוגמה, אפשר לצרף להודעה כותרות ולבחור קידוד תוכן.

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

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. 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, משנים את בורר הנתיב של /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.

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

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

הטמעה

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

קוד ה-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);
});

בהטמעה, הטיפול הזה צריך:

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

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

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

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

  • ב-server.js, משנים את בורר הנתיב של /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);
 
});