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

الخطوة الأولى هي الحصول على إذن من المستخدم لإرسال رسائل فورية إليه وبعد ذلك يمكننا الحصول على 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. نزِّل ملف Worker الخدمة.

  2. شغِّل JavaScript.

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

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

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

توافق متصفّح واجهة برمجة التطبيقات PushManager API

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

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

المصدر

طلب الإذن

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

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

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، يتكون من معلمين مطلوبين واختياريين.

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

خيارات userمرئي فقط

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

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

لتجنُّب هذا السيناريو ومنح مؤلفي المواصفات الوقت الكافي للتفكير في أفضل طريقة لتفعيل هذه الميزة، تمت إضافة الخيار 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 على جميع المعلومات المطلوبة اللازمة لإرسال رسائل الدفع إلى ذلك المستخدم. إذا كنت تطبع المحتوى باستخدام 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()، سيُرسِل المتصفح طلبات إلى الشبكة للخدمة بهدف retrieving the details that make up the PushSubscription.

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

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

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

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

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

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

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