שליחת הודעות באמצעות ספריות דחיפה באינטרנט

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

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

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

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

נעבור על השלבים הבאים:

  1. שולחים את המינוי לקצה העורפי שלנו ושומרים אותו.
  2. אחזור מינויים שנשמרו והפעלת הודעת Push.

שמירת מינויים

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

בדף האינטרנט לדוגמה, ה-PushSubscription נשלח לקצה העורפי שלנו באמצעות שליחת בקשת POST פשוטה:

function sendSubscriptionToBackEnd(subscription) {
  return fetch('/api/save-subscription/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(subscription),
  })
    .then(function (response) {
      if (!response.ok) {
        throw new Error('Bad status code from server.');
      }

      return response.json();
    })
    .then(function (responseData) {
      if (!(responseData.data && responseData.data.success)) {
        throw new Error('Bad response from server.');
      }
    });
}

לשרת Express בהדגמה שלנו יש מאזין בקשות תואם לנקודת הקצה /api/save-subscription/:

app.post('/api/save-subscription/', function (req, res) {

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

const isValidSaveRequest = (req, res) => {
  // Check the request body has at least an endpoint.
  if (!req.body || !req.body.endpoint) {
    // Not a valid subscription.
    res.status(400);
    res.setHeader('Content-Type', 'application/json');
    res.send(
      JSON.stringify({
        error: {
          id: 'no-endpoint',
          message: 'Subscription must have an endpoint.',
        },
      }),
    );
    return false;
  }
  return true;
};

אם המינוי תקין, אנחנו צריכים לשמור אותו ולהחזיר תגובת JSON מתאימה:

return saveSubscriptionToDatabase(req.body)
  .then(function (subscriptionId) {
    res.setHeader('Content-Type', 'application/json');
    res.send(JSON.stringify({data: {success: true}}));
  })
  .catch(function (err) {
    res.status(500);
    res.setHeader('Content-Type', 'application/json');
    res.send(
      JSON.stringify({
        error: {
          id: 'unable-to-save-subscription',
          message:
            'The subscription was received but we were unable to save it to our database.',
        },
      }),
    );
  });

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

function saveSubscriptionToDatabase(subscription) {
  return new Promise(function (resolve, reject) {
    db.insert(subscription, function (err, newDoc) {
      if (err) {
        reject(err);
        return;
      }

      resolve(newDoc._id);
    });
  });
}

שליחת הודעות דחיפה

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

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

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

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

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

const vapidKeys = {
  publicKey:
    'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
  privateKey: 'UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls',
};

בשלב הבא צריך להתקין את המודול web-push בשרת Node:

npm install web-push --save

לאחר מכן, בסקריפט Node שלנו אנחנו דורשים את המודול web-push כך:

const webpush = require('web-push');

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

const vapidKeys = {
  publicKey:
    'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
  privateKey: 'UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls',
};

webpush.setVapidDetails(
  'mailto:web-push-book@gauntface.com',
  vapidKeys.publicKey,
  vapidKeys.privateKey,
);

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

עכשיו מודול web-push מוכן לשימוש. השלב הבא הוא להפעיל הודעת דחיפה.

בהדגמה נעשה שימוש בחלונית האדמין המדומה כדי להפעיל הודעות דחיפה.

צילום מסך של דף הניהול.

לחיצה על הלחצן 'הפעלת הודעת דחיפה' תיצור בקשת POST אל /api/trigger-push-msg/, שהוא האות לקצה העורפי שלנו לשלוח הודעות דחיפה. לכן אנחנו יוצרים את המסלול ב-Express לנקודת הקצה הזו:

app.post('/api/trigger-push-msg/', function (req, res) {

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

return getSubscriptionsFromDatabase().then(function (subscriptions) {
  let promiseChain = Promise.resolve();

  for (let i = 0; i < subscriptions.length; i++) {
    const subscription = subscriptions[i];
    promiseChain = promiseChain.then(() => {
      return triggerPushMsg(subscription, dataToSend);
    });
  }

  return promiseChain;
});

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

const triggerPushMsg = function (subscription, dataToSend) {
  return webpush.sendNotification(subscription, dataToSend).catch((err) => {
    if (err.statusCode === 404 || err.statusCode === 410) {
      console.log('Subscription has expired or is no longer valid: ', err);
      return deleteSubscriptionFromDatabase(subscription._id);
    } else {
      throw err;
    }
  });
};

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

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

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

במקרה של שגיאה אחרת, פשוט throw err, וכך ההבטחה שתוחזר על ידי triggerPushMsg() תידחה.

נתייחס לחלק מקודי הסטטוס האחרים בקטע הבא, כשנבחן את פרוטוקול ה-Web Push בפירוט רב יותר.

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

.then(() => {
res.setHeader('Content-Type', 'application/json');
    res.send(JSON.stringify({ data: { success: true } }));
})
.catch(function(err) {
res.status(500);
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({
    error: {
    id: 'unable-to-send-messages',
    message: `We were unable to send messages to all subscriptions : ` +
        `'${err.message}'`
    }
}));
});

עברנו על שלבי ההטמעה העיקריים:

  1. יוצרים ממשק API כדי לשלוח מינויים מדף האינטרנט שלנו לקצה העורפי, כדי שאפשר יהיה לשמור אותם במסד נתונים.
  2. יוצרים ממשק API כדי להפעיל את שליחת ההתראות (במקרה הזה, ממשק API שנקרא מחלונית הניהול המזויפת).
  3. אחזור כל המינויים מהקצה העורפי שלנו ושליחת הודעה לכל מינוי באמצעות אחת מספריות ה-web-push.

ללא קשר לקצה העורפי (Node,‏ PHP,‏ Python וכו'), השלבים להטמעת Push יהיו זהים.

בשלב הבא, נסביר מה בדיוק הספריות האלה של דחיפה לאינטרנט עושות בשבילנו.

לאן ממשיכים

Code labs