إنشاء خادم للإشعارات الفورية

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

رمز العميل مكتمل بالفعل، وستعمل في هذا الدرس التطبيقي على الوظائف من جانب الخادم.

إنشاء ريمكس من نموذج التطبيق وعرضه في علامة تبويب جديدة

يتم حظر الإشعارات تلقائيًا من تطبيق Glitch المضمن، لذا لن تتمكن من معاينة التطبيق على هذه الصفحة. إليك ما يجب فعله بدلاً من ذلك:

  1. انقر على إنشاء ريمكس لتعديل لجعل المشروع قابلاً للتعديل.
  2. لمعاينة الموقع الإلكتروني، اضغط على عرض التطبيق. ثم اضغط ملء الشاشة ملء الشاشة

يتم فتح التطبيق المباشر في علامة تبويب جديدة على Chrome. في التطبيق المصغّر المضمّن، انقر على عرض المصدر لعرض الرمز مجددًا.

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

التعرُّف على تطبيق البداية ورمزه

ابدأ بإلقاء نظرة على واجهة مستخدم العميل للتطبيق.

في علامة تبويب Chrome الجديدة:

  1. اضغط على "Control+Shift+J" (أو "Command+Option+J" على أجهزة Mac) لفتح "أدوات مطوري البرامج". انقر على علامة التبويب وحدة التحكم.

  2. جرِّب النقر على الأزرار في واجهة المستخدم (راجع وحدة تحكم مطوّري البرامج في Chrome للحصول على النتائج).

    • من خلال تسجيل مشغّل الخدمات، يتم تسجيل مشغّل الخدمات في نطاق عنوان URL الخاص بمشروع Glitch. ويؤدي النقر على إلغاء تسجيل مشغّل الخدمات إلى إزالة مشغّل الخدمات. في حال إرفاق اشتراك في Push، سيتم أيضًا إيقاف اشتراك Push.

    • يؤدي الخيار الاشتراك في الإشعارات الفورية إلى إنشاء اشتراك فوري. ولا يتوفّر هذا الوضع إلا عند تسجيل مشغّل الخدمات مع توفُّر ثابت VAPID_PUBLIC_KEY في رمز العميل (سنتناول المزيد من التفاصيل لاحقًا)، لذا لا يمكنك النقر عليه بعد.

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

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

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

لنلقِ نظرة على ما يحدث على مستوى الخادم. وللاطّلاع على الرسائل الواردة من رمز الخادم، اطّلِع على سجلّ Node.js في واجهة Glitch.

  • في تطبيق Glitch، انقر على الأدوات -> السجلّات:

    ستظهر لك على الأرجح رسالة مثل Listening on port 3000.

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

    TODO: Implement sendNotifications()
    Endpoints to send to:  []
    

الآن لنلقِ نظرة على بعض التعليمات البرمجية.

  • يحتوي public/index.js على رمز العميل المكتمل. وهي ترصد الميزات وتسجِّل مشغّل الخدمات ويلغي تسجيلها، وتتحكّم في اشتراك المستخدم في تلقّي الإشعارات الفورية. ويتم أيضًا إرسال معلومات إلى الخادم حول الاشتراكات الجديدة والمحذوفة.

    ولأنك ستعمل فقط على وظائف الخادم، لن يتم تعديل هذا الملف (بخلاف تعبئة ثابت VAPID_PUBLIC_KEY).

  • "public/service-worker.js" هو مشغّل خدمات بسيط يلتقط الأحداث الفورية ويعرض الإشعارات.

  • يحتوي /views/index.html على واجهة المستخدم للتطبيق.

  • يحتوي ".env" على متغيّرات البيئة التي يحمّلها Glitch على خادم التطبيق عند بدء تشغيله. عليك تعبئة .env بتفاصيل المصادقة لإرسال الإشعارات.

  • "server.js" هو الملف الذي ستنفّذ فيه معظم عملك خلال هذا الدرس التطبيقي حول الترميز.

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

    في هذا الدرس التطبيقي حول الترميز، ستعمل على عناصر TODO هذه واحدًا تلو الآخر.

إنشاء تفاصيل VAPID وتحميلها

عنصر TODO الأول هو إنشاء تفاصيل VAPID وإضافتها إلى متغيرات بيئة Node.js وتعديل رمز العميل والخادم بالقيم الجديدة.

الخلفية

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

يُعرف البروتوكول الذي يجعل الإشعارات الفورية آمنة وخاصة باسم تحديد خادم التطبيقات الطوعي لخدمة Web Push (VAPID). يستخدم VAPID أسلوب التشفير باستخدام المفتاح العام للتحقّق من هوية التطبيقات والخوادم ونقاط نهاية الاشتراك، ولتشفير محتوى الإشعارات.

في هذا التطبيق، ستستخدم حزمة web-push npm لإنشاء مفاتيح VAPID وتشفير الإشعارات وإرسالها.

التنفيذ

في هذه الخطوة، أنشِئ مفتاحَي VAPID لتطبيقك وأضِفهما إلى متغيّرات البيئة. حمِّل متغيّرات البيئة في الخادم وأضِف المفتاح العام كم ثابت في رمز العميل.

  1. استخدِم الدالة generateVAPIDKeys في مكتبة web-push لإنشاء مفتاحَي VAPID.

    في server.js، أزِل التعليقات من سطور الرمز التالية:

    server.js

    // Generate VAPID keys (only do this once).
    /*
     * const vapidKeys = webpush.generateVAPIDKeys();
     * console.log(vapidKeys);
     */
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    
  2. بعد أن يُعيد تطبيق Glitch تشغيل تطبيقك، يتم إخراج المفاتيح التي تم إنشاؤها إلى سجلّ Node.js ضمن واجهة التطبيق (وليس إلى وحدة تحكّم Chrome). للاطلاع على مفاتيح VAPID، حدد أدوات -> السجلّات في واجهة Glitch.

    احرص على نسخ المفاتيح العامة والخاصة من مفتاحَي التشفير نفسهما.

    يُعيد تطبيق Glitch تشغيل تطبيقك في كل مرة تعدِّل فيها الرمز، لذا قد يتم عرض أول زوج من المفاتيح التي تنشئها بعيدًا عن شاشة العرض عند حدوث المزيد من النتائج.

  3. في .env، انسخ مفاتيح VAPID والصقها. يمكنك وضع المفاتيح بين علامتَي اقتباس ("...").

    بالنسبة إلى VAPID_SUBJECT، يمكنك إدخال "mailto:test@test.test".

    .env

    # process.env.SECRET
    VAPID_PUBLIC_KEY=
    VAPID_PRIVATE_KEY=
    VAPID_SUBJECT=
    VAPID_PUBLIC_KEY="BN3tWzHp3L3rBh03lGLlLlsq..."
    VAPID_PRIVATE_KEY="I_lM7JMIXRhOk6HN..."
    VAPID_SUBJECT="mailto:test@test.test"
    
  4. في server.js، ضَع تعليقات على هذين السطرين من الرمز مرة أخرى، لأنّك تحتاج إلى إنشاء مفاتيح VAPID مرة واحدة فقط.

    server.js

    // Generate VAPID keys (only do this once).
    /*
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    */
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    
  5. في server.js، حمِّل تفاصيل VAPID من متغيرات البيئة.

    server.js

    const vapidDetails = {
      // TODO: Load VAPID details from environment variables.
      publicKey: process.env.VAPID_PUBLIC_KEY,
      privateKey: process.env.VAPID_PRIVATE_KEY,
      subject: process.env.VAPID_SUBJECT
    }
    
  6. انسخ المفتاح العام والصقه في رمز العميل أيضًا.

    في public/index.js، أدخِل القيمة نفسها لـ VAPID_PUBLIC_KEY التي نسختها في ملف .env:

    public/index.js

    // Copy from .env
    const VAPID_PUBLIC_KEY = '';
    const VAPID_PUBLIC_KEY = 'BN3tWzHp3L3rBh03lGLlLlsq...';
    ````
    

تنفيذ وظيفة لإرسال الإشعارات

الخلفية

في هذا التطبيق، ستستخدم حزمة web-push npm لإرسال الإشعارات.

تعمل هذه الحزمة على تشفير الإشعارات تلقائيًا عند استدعاء webpush.sendNotification()، لذا لا داعي للقلق بشأن ذلك.

يقبل web-push خيارات متعددة للإشعارات - على سبيل المثال، يمكنك إرفاق عناوين بالرسالة وتحديد ترميز المحتوى.

في هذا الدرس التطبيقي حول الترميز، ستستخدم فقط خيارَين محدّدَين باستخدام سطور الرمز التالية:

let options = {
  TTL: 10000; // Time-to-live. Notifications expire after this.
  vapidDetails: vapidDetails; // VAPID keys from .env
};

يعمل الخيار TTL (مدة البقاء) على ضبط مهلة لانتهاء صلاحية الإشعار. وهذه إحدى الطرق التي يستخدمها الخادم لتجنب إرسال إشعار إلى المستخدم بعد أن يصبح غير ذا صلة.

يحتوي الخيار vapidDetails على مفاتيح VAPID التي حمَّلتها من متغيّرات البيئة.

التنفيذ

في server.js، عدِّل الدالة sendNotifications على النحو التالي:

server.js

function sendNotifications(database, endpoints) {
  // TODO: Implement functionality to send notifications.
  console.log('TODO: Implement sendNotifications()');
  console.log('Endpoints to send to: ', endpoints);
  let notification = JSON.stringify(createNotification());
  let options = {
    TTL: 10000, // Time-to-live. Notifications expire after this.
    vapidDetails: vapidDetails // VAPID keys from .env
  };
  endpoints.map(endpoint => {
    let subscription = database[endpoint];
    webpush.sendNotification(subscription, notification, options);
  });
}

بما أنّ webpush.sendNotification() يرسل وعودًا، يمكنك بسهولة إضافة خيار معالجة الأخطاء.

في server.js، عدِّل الدالة sendNotifications مرة أخرى:

server.js

function sendNotifications(database, endpoints) {
  let notification = JSON.stringify(createNotification());
  let options = {
    TTL: 10000; // Time-to-live. Notifications expire after this.
    vapidDetails: vapidDetails; // VAPID keys from .env
  };
  endpoints.map(endpoint => {
    let subscription = database[endpoint];
    webpush.sendNotification(subscription, notification, options);
    let id = endpoint.substr((endpoint.length - 8), endpoint.length);
    webpush.sendNotification(subscription, notification, options)
    .then(result => {
      console.log(`Endpoint ID: ${id}`);
      console.log(`Result: ${result.statusCode} `);
    })
    .catch(error => {
      console.log(`Endpoint ID: ${id}`);
      console.log(`Error: ${error.body} `);
    });
  });
}

التعامل مع الاشتراكات الجديدة

الخلفية

إليك ما يحدث عندما يشترك المستخدم في خدمة تلقّي الإشعارات الفورية:

  1. ينقر المستخدم على الاشتراك في الدفعة.

  2. يستخدم العميل المتغير VAPID_PUBLIC_KEY الثابت (مفتاح VAPID العام للخادم) لإنشاء كائن subscription فريد خاص بالخادم. يبدو كائن subscription على النحو التالي:

       {
         "endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9...",
         "expirationTime": null,
         "keys":
         {
           "p256dh": "BNYDjQL9d5PSoeBurHy2e4d4GY0sGJXBN...",
           "auth": "0IyyvUGNJ9RxJc83poo3bA"
         }
       }
    
  3. يرسِل العميل طلب "POST" إلى عنوان URL الخاص بـ "/add-subscription"، بما في ذلك الاشتراك على شكل JSON مُدمَج في النص.

  4. يسترد الخادم subscription المسلسلة من نص طلب POST، ويحلّلها مرة أخرى إلى ملف JSON، ثم يضيفها إلى قاعدة بيانات الاشتراكات.

    تخزِّن قاعدة البيانات الاشتراكات باستخدام نقاط النهاية الخاصة بها كمفتاح:

    {
      "https://fcm...1234": {
        endpoint: "https://fcm...1234",
        expirationTime: ...,
        keys: { ... }
      },
      "https://fcm...abcd": {
        endpoint: "https://fcm...abcd",
        expirationTime: ...,
        keys: { ... }
      },
      "https://fcm...zxcv": {
        endpoint: "https://fcm...zxcv",
        expirationTime: ...,
        keys: { ... }
      },
    }

والآن، يتوفر الاشتراك الجديد للخادم لإرسال الإشعارات.

التنفيذ

تظهر طلبات الاشتراكات الجديدة في مسار /add-subscription، وهو عنوان URL لصفحة POST. سيظهر لك معالِج مسار بديل في server.js:

server.js

app.post('/add-subscription', (request, response) => {
  // TODO: implement handler for /add-subscription
  console.log('TODO: Implement handler for /add-subscription');
  console.log('Request body: ', request.body);
  response.sendStatus(200);
});

عند التنفيذ، يجب أن يستوفي هذا المعالج ما يلي:

  • يمكنك استرداد الاشتراك الجديد من نص الطلب.
  • الوصول إلى قاعدة بيانات الاشتراكات النشطة
  • أضِف الاشتراك الجديد إلى قائمة الاشتراكات النشطة.

للتعامل مع الاشتراكات الجديدة:

  • في server.js، عدِّل معالج المسار لـ /add-subscription على النحو التالي:

    server.js

    app.post('/add-subscription', (request, response) => {
      // TODO: implement handler for /add-subscription
      console.log('TODO: Implement handler for /add-subscription');
      console.log('Request body: ', request.body);
      let subscriptions = Object.assign({}, request.session.subscriptions);
      subscriptions[request.body.endpoint] = request.body;
      request.session.subscriptions = subscriptions;
      response.sendStatus(200);
    });

التعامل مع عمليات إلغاء الاشتراك

الخلفية

لن يعرف الخادم دائمًا الحالات التي يصبح فيها الاشتراك غير نشط، على سبيل المثال، قد يتم حجب بيانات اشتراك عندما يوقف المتصفح عامل الخدمة.

ومع ذلك، يمكن للخادم معرفة الاشتراكات التي تم إلغاؤها من خلال واجهة مستخدم التطبيق. في هذه الخطوة، ستقوم بتنفيذ وظائف لإزالة اشتراك من قاعدة البيانات.

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

التنفيذ

تصل طلبات إلغاء الاشتراكات إلى عنوان URL لصفحة POST /remove-subscription.

يظهر معالِج مسار التنويه في server.js على النحو التالي:

server.js

app.post('/remove-subscription', (request, response) => {
  // TODO: implement handler for /remove-subscription
  console.log('TODO: Implement handler for /remove-subscription');
  console.log('Request body: ', request.body);
  response.sendStatus(200);
});

عند التنفيذ، يجب أن يستوفي هذا المعالج ما يلي:

  • استرداد نقطة نهاية الاشتراك المُلغى من نص الطلب
  • الوصول إلى قاعدة بيانات الاشتراكات النشطة
  • أزِل الاشتراك المُلغى من قائمة الاشتراكات النشطة.

يحتوي نص طلب POST المقدَّم من العميل على نقطة النهاية التي يجب إزالتها:

{
  "endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9..."
}

للتعامل مع عمليات إلغاء الاشتراك:

  • في server.js، عدِّل معالج المسار لـ /remove-subscription على النحو التالي:

    server.js

  app.post('/remove-subscription', (request, response) => {
    // TODO: implement handler for /remove-subscription
    console.log('TODO: Implement handler for /remove-subscription');
    console.log('Request body: ', request.body);
    let subscriptions = Object.assign({}, request.session.subscriptions);
    delete subscriptions[request.body.endpoint];
    request.session.subscriptions = subscriptions;
    response.sendStatus(200);
  });