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

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

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

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

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

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

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

שמירת המינויים מתבצעת

שמירת הנתונים של 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);
    });
  });
}

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

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

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

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

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

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

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

בשלב הבא, עלינו להתקין את המודול web-push עבור שרת הצמתים שלנו:

npm install web-push --save

לאחר מכן, בסקריפט הצומת שלנו, אנחנו דורשים את המודול 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. המידע הזה יישלח בפועל לשירות ה-push באינטרנט כחלק מהבקשה להפעלת Push. הסיבה לכך היא שאם שירות דחיפה באינטרנט צריך ליצור קשר עם השולח, יש לו מידע שיאפשר לו לעשות זאת.

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

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

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

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

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().

בקטע הבא נסביר בפירוט על כמה מקודי הסטטוס האחרים.

לאחר מעבר בין המינויים, אנחנו צריכים להחזיר תגובת 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 יהיו זהים.

בשלב הבא, מה בדיוק ספריות ה-Web-push האלה עושות בשבילנו?

השלבים הבאים

שיעורי Lab