الاشتراك في قناة مستخدم

الخطوة الأولى هي الحصول على إذن من المستخدم لإرسال رسائل فورية إليه، وبعد ذلك يمكننا الحصول على PushSubscription.

إنّ واجهة برمجة التطبيقات JavaScript API لتنفيذ ذلك بسيطة إلى حدٍ ما، لذا لنطّلِع على مسار المنطق.

رصد الميزات

أولاً، علينا التحقّق مما إذا كان المتصفّح الحالي يتيح رسائل الإشعارات الفورية. يمكننا التحقّق مما إذا كانت ميزة "الدفع الفوري" متاحة من خلال إجراء تحقّقَين بسيطَين.

  1. تحقَّق من serviceWorker في navigator.
  2. ابحث عن PushManager في window.
if (!('serviceWorker' in navigator)) {
  // Service Worker isn't supported on this browser, disable or hide UI.
  return;
}

if (!('PushManager' in window)) {
  // Push isn't supported on this browser, disable or hide UI.
  return;
}

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

تسجيل مشغّل خدمات

من خلال ميزة رصد الميزات، نعرف أنّ كلاً من مهام الخدمة وPush متوافقان. الخطوة التالية هي "تسجيل" عامل الخدمة.

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

لتسجيل مشغّل خدمات، يمكنك الاتصال بـ navigator.serviceWorker.register() مع تضمين المسار إلىملفنا. على النحو التالي:

function registerServiceWorker() {
  return navigator.serviceWorker
    .register('/service-worker.js')
    .then(function (registration) {
      console.log('Service worker successfully registered.');
      return registration;
    })
    .catch(function (err) {
      console.error('Unable to register service worker.', err);
    });
}

تُعلم هذه الدالة المتصفّح بأنّ لدينا ملف مشغّل خدمات ومكانه. في هذه الحالة، يكون ملف الخدمة في /service-worker.js. في الخلفية، سينفّذ المتصفّح الخطوات التالية بعد استدعاء register():

  1. نزِّل ملف الخدمة العاملة.

  2. شغِّل JavaScript.

  3. إذا كان كل شيء يعمل بشكل صحيح ولم تظهر أي أخطاء، سيتم حلّ الوعد الذي يعرضه register(). في حال حدوث أي أخطاء، سيتم رفض الوعد.

إذا رفضت register()، تحقّق جيدًا من JavaScript بحثًا عن أخطاء إملائية أو أخطاء في "أدوات مطوّري البرامج في Chrome".

عندما يتم حلّ register()، يتم عرض ServiceWorkerRegistration. سنستخدم عملية التسجيل هذه للوصول إلى PushManager API.

توافق متصفّح PushManager API

توافق المتصفّح

  • Chrome: 42
  • ‫Edge: 17
  • Firefox: 44
  • ‫Safari: 16

المصدر

طلب الإذن

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

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

function askPermission() {
  return new Promise(function (resolve, reject) {
    const permissionResult = Notification.requestPermission(function (result) {
      resolve(result);
    });

    if (permissionResult) {
      permissionResult.then(resolve, reject);
    }
  }).then(function (permissionResult) {
    if (permissionResult !== 'granted') {
      throw new Error("We weren't granted permission.");
    }
  });
}

في الرمز أعلاه، المقتطف المهم من الرمز هو طلب Notification.requestPermission(). ستعرض هذه الطريقة طلبًا للمستخدم:

طلب الإذن على متصفّح Chrome على أجهزة الكمبيوتر المكتبي والأجهزة الجوّالة

بعد تفاعل المستخدم مع طلب الإذن من خلال الضغط على "السماح" أو "الحظر" أو إغلاقه فقط، ستظهر لنا النتيجة على شكل سلسلة: 'granted' أو 'default' أو 'denied'.

في نموذج الرمز البرمجي أعلاه، يتم حلّ الوعد الذي يعرضه askPermission() في حال منح الإذن، وإلا سنُعرِض خطأ يؤدي إلى رفض الوعد.

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

والخبر السار هو أنّ معظم المستخدمين يوافقون على منح الأذونات طالما يعرفون سبب طلبها.

سنلقي نظرة على كيفية طلب بعض المواقع الإلكترونية الشائعة الحصول على الإذن لاحقًا.

اشتراك مستخدم باستخدام PushManager

بعد تسجيل عامل الخدمة والحصول على الإذن، يمكننا اشتراك مستخدم من خلال الاتصال بالرقم registration.pushManager.subscribe().

function subscribeUserToPush() {
  return navigator.serviceWorker
    .register('/service-worker.js')
    .then(function (registration) {
      const subscribeOptions = {
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(
          'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
        ),
      };

      return registration.pushManager.subscribe(subscribeOptions);
    })
    .then(function (pushSubscription) {
      console.log(
        'Received PushSubscription: ',
        JSON.stringify(pushSubscription),
      );
      return pushSubscription;
    });
}

عند استدعاء الطريقة subscribe()، نُرسل كائن options يتألف من المَعلمات المطلوبة والاختيارية.

لنلقِ نظرة على جميع الخيارات التي يمكننا تمريرها.

خيارات userVisibleOnly

عند إضافة رسائل الإشعارات الفورية إلى المتصفّحات لأول مرة، لم يكن هناك يقين بشأن ما إذا كان يجب أن يتمكّن المطوّرون من إرسال رسالة إشعار فوري بدون عرض إشعار. يُشار إلى ذلك عادةً باسم "إرسال إشعار بدون إشعار"، لأنّ المستخدم لا يعلم أنّه حدث شيء في الخلفية.

وكان القلق هو أنّه يمكن للمطوّرين تنفيذ إجراءات غير مرغوب فيها، مثل تتبُّع الموقع الجغرافي للمستخدم بشكلٍ مستمر بدون علمه.

لتجنُّب هذا السيناريو ومنح مؤلفي المواصفات الوقت الكافي للتفكير في أفضل طريقة لتفعيل هذه الميزة، تمت إضافة الخيار userVisibleOnly، وتمثل القيمة true رمزًا لعقد مع المتصفح يقضي بأن يعرض تطبيق الويب إشعارًا في كل مرة يتم فيها تلقّي إشعار فوري (أي لا يتم إرسال إشعار فوري بدون إشعار).

في الوقت الحالي، يجب إدخال قيمة true. إذا لم تُدرِج مفتاح userVisibleOnly أو لم تُدخل false، ستظهر لك رسالة الخطأ التالية:

لا يتيح Chrome حاليًا استخدام واجهة برمجة التطبيقات Push API إلا للاشتراكات التي ستؤدي إلى إرسال رسائل ظاهرة للمستخدم. يمكنك الإشارة إلى ذلك من خلال الاتصال برقم pushManager.subscribe({userVisibleOnly: true}) بدلاً من ذلك. يُرجى الاطّلاع على https://goo.gl/yqv4Q4 لمعرفة المزيد من التفاصيل.

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

خيار applicationServerKey

لقد ذكرنا "مفاتيح خادم التطبيقات" بإيجاز في القسم السابق. تستخدم خدمة الإشعارات الفورية "مفاتيح التطبيق الخادم" لتحديد التطبيق الذي يشترك فيه المستخدم و ضمان أنّ التطبيق نفسه هو الذي يرسل الرسائل إلى هذا المستخدم.

مفاتيح خادم التطبيقات هي زوج من المفتاحَين العام والخاص الفريدَين لتطبيقك. يجب الحفاظ على سرية المفتاح الخاص لتطبيقك، ويمكن مشاركة المفتاح العام بحرية.

الخيار applicationServerKey الذي تم تمريره إلى طلب subscribe() هو مفتاح العميل المتاح للجميع في التطبيق. ويُرسِل المتصفّح هذه المعلومات إلى خدمة الإشعارات الفورية عند اشتراك المستخدم، ما يعني أنّ خدمة الإشعارات الفورية يمكنها ربط المفتاح العام لتطبيقك بـ PushSubscription المستخدم.

يوضّح الرسم البياني أدناه هذه الخطوات.

  1. يتم تحميل تطبيق الويب في متصفّح، ويمكنك الاتصال بخدمة subscribe()، مع إدخال مفتاح خادم التطبيق العام.
  2. بعد ذلك، يُرسِل المتصفّح طلبًا على الشبكة إلى خدمة دفع ستُنشئ نقطة نهاية، وتربط هذه النقطة بتطبيق المفتاح العام وتُعيد النقطة إلى المتصفّح.
  3. سيضيف المتصفّح نقطة النهاية هذه إلى PushSubscription، والتي يتم عرضها من خلال الوعد subscribe().

صورة توضيحية لاستخدام مفتاح خادم التطبيق العام في أسلوب الاشتراك

عندما تريد إرسال رسالة فورية لاحقًا، عليك إنشاء عنوان Authorization الذي سيتضمّن معلومات موقَّعة باستخدام المفتاح الخاص لخادم التطبيقات. عندما تتلقّى خدمة الإشعارات الفورية طلبًا لإرسال رسالة إشعار فوري، يمكنها التحقّق من صحة عنوان التفويض الموقَّع من خلال البحث عن المفتاح العام المرتبط بالنقطة الطرفية التي تتلقّى الطلب. إذا كان التوقيع صالحًا، تعرف خدمة الدفع أنّه من المفترض أن يكون قد صدر من خادم التطبيق الذي يتضمّن المفتاح الخاص المطابق. إنّه إجراء أمان يمنع أي مستخدم آخر من إرسال الرسائل إلى مستخدمي التطبيق.

كيفية استخدام مفتاح خادم التطبيق الخاص عند إرسال
رسالة

من الناحية الفنية، يكون applicationServerKey اختياريًا. ومع ذلك، يتطلّب أبسط شكل من أشكال التنفيذ على Chrome ذلك، وقد تتطلّب المتصفحات الأخرى ذلك في المستقبل. هذا الإجراء اختياري في Firefox.

إنّ المواصفة التي تحدّد ما يجب أن يكون عليه مفتاح خادم التطبيق هي مواصفة VAPID. عندما تقرأ شيئًا يشير إلى "مفاتيح خادم التطبيق" أو "مفاتيح VAPID"، تذكَّر أنّهما يشيران إلى الشيء نفسه.

كيفية إنشاء مفاتيح خادم التطبيقات

يمكنك إنشاء مجموعة عامة وخاصة من مفاتيح خادم التطبيقات من خلال الانتقال إلى web-push-codelab.glitch.me أو يمكنك استخدام سطر الأوامر web-push لإنشاء مفاتيح من خلال اتّباع الخطوات التالية:

    $ npm install -g web-push
    $ web-push generate-vapid-keys

ما عليك سوى إنشاء هذه المفاتيح مرة واحدة لتطبيقك، فقط احرص على إبقاء المفتاح الخاص سريًا. (نعم، لقد قلتُ ذلك للتو).

الأذونات وsubscribe()

هناك تأثير جانبي واحد للاتصال برقم subscribe(). إذا لم يكن تطبيق الويب الخاص بك يملك أذونات لعرض الإشعارات في وقت الاتصال subscribe()، سيطلب المتصفّح منك منح الأذونات. يكون ذلك مفيدًا إذا كانت واجهة المستخدم متوافقة مع هذه العملية، ولكن إذا كنت تريد مزيدًا من التحكّم (وأعتقد أنّ معظم المطوّرين سيفعلون ذلك)، عليك استخدام Notification.requestPermission() API التي استخدمناها سابقًا.

ما هو PushSubscription؟

نُطلِق subscribe() ونُمرِّر بعض الخيارات، وفي المقابل نحصل على وعد يُحلّ إلى PushSubscription، ما يؤدي إلى ظهور بعض الرموز البرمجية على النحو التالي:

function subscribeUserToPush() {
  return navigator.serviceWorker
    .register('/service-worker.js')
    .then(function (registration) {
      const subscribeOptions = {
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(
          'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
        ),
      };

      return registration.pushManager.subscribe(subscribeOptions);
    })
    .then(function (pushSubscription) {
      console.log(
        'Received PushSubscription: ',
        JSON.stringify(pushSubscription),
      );
      return pushSubscription;
    });
}

يحتوي العنصر PushSubscription على جميع المعلومات المطلوبة لإرسال رسائل push إلى هذا المستخدم. في حال طباعة المحتوى باستخدام JSON.stringify()، سيظهر لك ما يلي:

    {
      "endpoint": "https://some.pushservice.com/something-unique",
      "keys": {
        "p256dh":
    "BIPUL12DLfytvTajnryr2PRdAgXS3HGKiLqndGcJGabyhHheJYlNGCeXl1dn18gSJ1WAkAPIxr4gK0_dQds4yiI=",
        "auth":"FPssNDTKnInHVndSTdbKFw=="
      }
    }

endpoint هو عنوان URL لخدمات الدفع. لتشغيل رسالة فورية، أرسِل طلب POST إلى عنوان URL هذا.

يحتوي العنصر keys على القيم المستخدَمة لتشفير بيانات الرسائل المُرسَلة مع رسالة فورية (سنناقشها لاحقًا في هذا القسم).

إعادة الاشتراك بانتظام لمنع انتهاء صلاحيته

عند الاشتراك في الإشعارات الفورية، غالبًا ما تتلقّى PushSubscription.expirationTime من null. من الناحية النظرية، يعني ذلك أنّ الاشتراك لا ينتهي أبدًا (على عكس ما يحدث عندما تتلقّى DOMHighResTimeStamp، الذي يُعلمك بالوقت المحدد لانتهاء صلاحية الاشتراك). في الممارسة العملية، من الشائع أن تسمح المتصفّحات لانتهاء صلاحية الاشتراكات، على سبيل المثال، إذا لم يتم استلام إشعارات فورية لفترة أطول، أو إذا رصد المتصفّح أنّ المستخدم لا يستخدم تطبيقًا لديه إذن الإشعارات الفورية. ومن الأنماط التي يمكن من خلالها منع حدوث ذلك إعادة اشتراك المستخدم عند تلقّي كل إشعار، كما هو موضّح في المقتطف التالي. يتطلّب ذلك منك إرسال الإشعارات بشكل متكرّر بما يكفي كي لا ينتهي صلاحية الاشتراك تلقائيًا في المتصفّح، ويجب أن توازن بعناية مزايا وعيوب احتياجات الإشعارات المشروعة مقابل إرسال إشعارات غير مرغوب فيها للمستخدم فقط لمنع انتهاء صلاحية الاشتراك. في النهاية، يجب عدم محاولة إيقاف المتصفّح في جهوده لحماية المستخدم من اشتراكات الإشعارات التي تم نسيان بعضها منذ زمن طويل.

/* In the Service Worker. */

self.addEventListener('push', function(event) {
  console.log('Received a push message', event);

  // Display notification or handle data
  // Example: show a notification
  const title = 'New Notification';
  const body = 'You have new updates!';
  const icon = '/images/icon.png';
  const tag = 'simple-push-demo-notification-tag';

  event.waitUntil(
    self.registration.showNotification(title, {
      body: body,
      icon: icon,
      tag: tag
    })
  );

  // Attempt to resubscribe after receiving a notification
  event.waitUntil(resubscribeToPush());
});

function resubscribeToPush() {
  return self.registration.pushManager.getSubscription()
    .then(function(subscription) {
      if (subscription) {
        return subscription.unsubscribe();
      }
    })
    .then(function() {
      return self.registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY_HERE')
      });
    })
    .then(function(subscription) {
      console.log('Resubscribed to push notifications:', subscription);
      // Optionally, send new subscription details to your server
    })
    .catch(function(error) {
      console.error('Failed to resubscribe:', error);
    });
}

إرسال اشتراك إلى خادمك

بعد الحصول على اشتراك في خدمة "الدفع الفوري"، عليك إرساله إلى خادمك. يعود الأمر إليك في تحديد كيفية تنفيذ ذلك، ولكن ننصحك باستخدام JSON.stringify() للحصول على جميع البيانات اللازمة من عنصر الاشتراك. بدلاً من ذلك، يمكنك تجميع النتيجة نفسها يدويًا على النحو التالي:

const subscriptionObject = {
  endpoint: pushSubscription.endpoint,
  keys: {
    p256dh: pushSubscription.getKeys('p256dh'),
    auth: pushSubscription.getKeys('auth'),
  },
};

// The above is the same output as:

const subscriptionObjectToo = JSON.stringify(pushSubscription);

يتم إرسال الاشتراك في صفحة الويب على النحو التالي:

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.');
      }
    });
}

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

app.post('/api/save-subscription/', function (req, res) {
  if (!isValidSaveRequest(req, res)) {
    return;
  }

  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.',
          },
        }),
      );
    });
});

باستخدام تفاصيل PushSubscription على خادمنا، يمكننا إرسال رسالة إلى المستخدم متى شئنا.

إعادة الاشتراك بانتظام لمنع انتهاء صلاحيته

عند الاشتراك في الإشعارات الفورية، غالبًا ما تتلقّى PushSubscription.expirationTime من null. من الناحية النظرية، يعني ذلك أنّ الاشتراك لا ينتهي أبدًا (على عكس ما يحدث عند تلقّي DOMHighResTimeStamp، الذي يُعلمك بالوقت المحدد لانتهاء صلاحية الاشتراك). في الممارسة العملية، من الشائع أن تسمح المتصفّحات لانتهاء صلاحية الاشتراكات، على سبيل المثال، إذا لم يتم استلام إشعارات فورية لفترة طويلة، أو إذا رصد المتصفّح أنّ المستخدم لا يستخدم التطبيق الذي لديه إذن الإشعارات الفورية. ومن الأنماط التي يمكن من خلالها منع حدوث ذلك إعادة اشتراك المستخدم عند تلقّي كل إشعار، كما هو موضّح في المقتطف التالي. يتطلّب ذلك منك إرسال الإشعارات بشكل متكرّر بما يكفي لكي لا ينتهي صلاحية الاشتراك تلقائيًا في المتصفّح، ويجب أن توازن بعناية مزايا وعيوب احتياجات الإشعارات المشروعة مقابل إرسال إشعارات غير مرغوب فيها للمستخدم فقط لمنع انتهاء صلاحية الاشتراك. في النهاية، يجب عدم محاولة إيقاف المتصفّح في جهوده لحماية المستخدم من اشتراكات الإشعارات التي تم نسيان بعضها منذ زمن طويل.

/* In the Service Worker. */

self.addEventListener('push', function(event) {
  console.log('Received a push message', event);

  // Display notification or handle data
  // Example: show a notification
  const title = 'New Notification';
  const body = 'You have new updates!';
  const icon = '/images/icon.png';
  const tag = 'simple-push-demo-notification-tag';

  event.waitUntil(
    self.registration.showNotification(title, {
      body: body,
      icon: icon,
      tag: tag
    })
  );

  // Attempt to resubscribe after receiving a notification
  event.waitUntil(resubscribeToPush());
});

function resubscribeToPush() {
  return self.registration.pushManager.getSubscription()
    .then(function(subscription) {
      if (subscription) {
        return subscription.unsubscribe();
      }
    })
    .then(function() {
      return self.registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY_HERE')
      });
    })
    .then(function(subscription) {
      console.log('Resubscribed to push notifications:', subscription);
      // Optionally, send new subscription details to your server
    })
    .catch(function(error) {
      console.error('Failed to resubscribe:', error);
    });
}

الأسئلة الشائعة

في ما يلي بعض الأسئلة الشائعة التي طرحها المستخدمون في هذه المرحلة:

هل يمكنني تغيير خدمة الإشعارات الفورية التي يستخدمها المتصفّح؟

لا، يختار المتصفح خدمة الإشعارات الفورية، وكما رأينا في subscribe()، سيُرسِل المتصفح طلبات إلى الشبكة من أجل خدمة الإشعارات الفورية بهدف استرجاع التفاصيل التي تشكّل PushSubscription.

يستخدم كل متصفّح خدمة دفع مختلفة، أليس لديها واجهات برمجة تطبيقات مختلفة؟

ستتوقع جميع خدمات الإشعارات الفورية استخدام واجهة برمجة التطبيقات نفسها.

تُعرف واجهة برمجة التطبيقات الشائعة هذه باسم Web Push Protocol وتوضّح طلب الشبكة الذي سيحتاج تطبيقك إلى إجرائه لتشغيل رسالة فورية.

إذا اشتركتُ مستخدمًا على جهاز الكمبيوتر المكتبي، هل سيتم اشتراكه على هاتفه أيضًا؟

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

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

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