إرسال الرسائل باستخدام مكتبات الإشعارات الفورية على الويب

تتمثل إحدى المشكلات عند العمل باستخدام دفع الويب في أن تشغيل رسالة الدفع "مبالغة في التفكير". لتشغيل رسالة دفع، يحتاج أحد التطبيقات إلى تقديم طلب POST إلى خدمة دفع باتّباع بروتوكول الدفع على الويب. لاستخدام ميزة الدفع في جميع المتصفحات، يجب استخدام VAPID (تُعرف أيضًا باسم مفاتيح خادم التطبيقات) والتي تتطلّب بشكل أساسي إعداد عنوان مع قيمة تثبت قدرة التطبيق على إرسال رسالة إلى المستخدم. لإرسال البيانات باستخدام رسالة فورية، يجب تشفير البيانات وإضافة عناوين معيّنة لكي يتمكّن المتصفّح من فك تشفير الرسالة بشكل صحيح.

المشكلة الرئيسية في تشغيل الدفع هي أنه إذا واجهت مشكلة، يصعُب تشخيصها. يتحسن هذا الأمر مع الوقت وإتاحة المزيد من المتصفحات، ولكنه ليس سهلاً. لهذا السبب، أنصحك بشدة باستخدام مكتبة للتعامل مع تشفير الرسالة الفورية وتنسيقها وتشغيلها.

إذا كنت تريد فعلاً التعرّف على ما تفعله المكتبات، سنتناول ذلك في القسم التالي. والآن، سننظر في إدارة الاشتراكات واستخدام مكتبة حالية لإرسال طلبات الدفع عبر الويب.

سنستخدم في هذا القسم مكتبة عُقدة الدفع على الويب. سيكون للغات الأخرى اختلافات، لكنها لن تكون مختلفة جدًا. نحن نبحث عن العقدة بسبب أنها 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 وتشغيل الرمز لعرض رسالة الدفع.

يحتوي العرض التوضيحي على صفحة "إعجاب المشرف" التي تتيح لك إجراء دفعة. ونظرًا لأنه مجرد عرض توضيحي، فهو صفحة عامة.

سأنتقل إلى كل خطوة لإدارتها في تشغيل العرض التوضيحي. ستكون هذه خطوات صغيرة حتى يتمكن الجميع من المتابعة، بما في ذلك أي شخص جديد في Node.

عندما ناقشنا اشتراك مستخدم، تناولنا إضافة applicationServerKey إلى خيارات subscribe(). نحتاج إلى هذا المفتاح الخاص في الخلفية.

في العرض التوضيحي، تتم إضافة هذه القيم إلى تطبيق Node على هذا النحو (الكود الممل الذي أعرفه، ولكن أريدك فقط أن تعرف أنه لا يوجد سحر):

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. وفي الواقع، سيتم إرسال هذه المعلومات إلى خدمة Web Push كجزء من طلب بدء الدفع. السبب في ذلك هو أنه في حالة احتياج إحدى خدمات الويب الفورية للتواصل مع المرسل، تكون لديها بعض المعلومات التي ستمكّنها من ذلك.

وبذلك، تكون وحدة 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 لا يزال صالحًا أم لا.

لتحديد نوع الخطأ من خدمة الإرسال التلقائي، من الأفضل مراجعة رمز الحالة. تختلف رسائل الخطأ بين خدمات الدفع وبعضها أكثر فائدة من غيرها.

في هذا المثال، تتحقق الأداة من رمزَي الحالة 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. يمكنك إنشاء واجهة برمجة تطبيقات لإرسال الاشتراكات من صفحتنا على الويب إلى واجهتنا الخلفية بحيث يمكن حفظها في قاعدة بيانات.
  2. أنشئ واجهة برمجة تطبيقات لبدء إرسال الرسائل الفورية (في هذه الحالة، يتم استدعاء واجهة برمجة التطبيقات من لوحة المشرف الظاهرة).
  3. يمكنك استرداد جميع الاشتراكات من الخلفية وإرسال رسالة إلى كل اشتراك باستخدام إحدى مكتبات الدفع الفورية.

بغض النظر عن الخلفية (العقدة، لغة PHP، بايثون، ...)، ستكون خطوات تنفيذ الدفع واحدة.

بعد ذلك، ما الذي تفعله لنا مكتبات الدفع على الويب هذه تحديدًا؟

الخطوات التالية

الدروس التطبيقية حول الترميز