اذهب إلى المحتوى

استخدام مرسل الأحداث Event emitter في Node.js


Hassan Hedr

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

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

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

المستلزمات

هذا المقال جزء من سلسلة دليل تعلم Node.js لذا يجب قبل قراءته:

  • تثبيت بيئة Node.js على الجهاز، حيث استخدمنا في هذا المقال الإصدار رقم 10.20.1.
  • معرفة بأساسيات استخدام الأصناف في جافاسكربت، حيث سنستخدمها ضمن الأمثلة في هذا المقال وهي متوفرة في جافاسكربت منذ الإصدار ES2015 أو ES6.

إرسال أحداث Emitting Events

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

ولنبدأ بالتعرف على طريقة استخدام كائن مرسل أحداث منفصل، ونبدأ أولًا بإنشاء مجلد للمشروع بالاسم ‎event-emitters‎ كالتالي:

$ mkdir event-emitters

وندخل إلى المجلد:

$ cd event-emitters

نُنشئ ملف جافاسكربت جديد بالاسم ‎firstEventEmitter.js‎ ونفتحه ضمن أي محرر نصوص، حيث سنستخدم في أمثلتنا محرر ‎nano‎ كالتالي:

$ nano firstEventEmitter.js

يمكن استخدام الصنف ‎EventEmitter‎ الموجود ضمن الوحدة ‎events‎ في نود لإرسال الأحداث، ولنبدأ باستيراد ذلك الصنف من تلك الوحدة كالتالي:

const { EventEmitter } = require("events");

ثم ننشئ كائنًا جديدًا من ذلك الصنف:

const { EventEmitter } = require("events");

const firstEmitter = new EventEmitter();

ونختبر إرسال حدث ما من هذا الكائن كالتالي:

const { EventEmitter } = require("events");

const firstEmitter = new EventEmitter();


firstEmitter.emit("My first event");

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

ملاحظة: يعيد تنفيذ تابع الإرسال ‎emit()‎ قيمة منطقية تكون صحيحة ‎true‎ في حال كان هناك أي تابع يستمع لذلك الحدث، وفي حال لم يكن هناك أي مستمع سيعيد القيمة ‎false‎ رغم عدم توفر معلومات أخرى عن المستمعين.

والآن نحفظ الملف وننفذه باستخدام الأمر ‎node‎ ونلاحظ النتيجة:

$ node firstEventEmitter.js

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

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

نبدأ بإنشاء مدير البطاقات حيث سيرث صنف مرسل الأحداث الأساسي ‎EventEmitter‎ مباشرة كي لا نضطر لإنشاء كائن مرسل للأحداث منفصل داخليًا واستخدامه، ونٌنشئ ملف جافاسكربت جديد بالاسم ‎ticketManager.js‎:

$ nano ticketManager.js

كما فعلنا سابقًا نستورد الصنف ‎EventEmitter‎ من الوحدة ‎events‎ لاستخدامه كالتالي:

const EventEmitter = require("events");

ونعرف صنف مدير البطاقات ‎TicketManager‎ الذي سيوفر تابع الشراء لاحقًا:

const EventEmitter = require("events");

class TicketManager extends EventEmitter {}

نلاحظ أن صنف مدير البطاقات ‎TicketManager‎ يرث من صنف مرسل الأحداث الأساسي ‎EventEmitter‎ ما يعني أنه سيرث كل التوابع والخواص التي يوفرها صنف مرسل الأحداث وبالتالي يمكننا استدعاء تابع إرسال الأحداث ‎emit()‎ من الصنف نفسه مباشرةً.

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

const EventEmitter = require("events");

class TicketManager extends EventEmitter {
    constructor(supply) {
        super();
        this.supply = supply;
    }
}

يقبل التابع الباني معامل العدد ‎supply‎ والذي يعبر عن الكمية المتوفرة للبيع، وبما أن الصنف ‎TicketManager‎ يرث من صنف مرسل الأحداث الأساسي ‎EventEmitter‎ فيجب استدعاء التابع الباني للصنف الأب عبر استدعاء ‎super()‎ وذلك لتهيئة توابع وخاصيات الصنف الأب بشكل صحيح.

وبعد ذلك نعرف قيمة خاصية الكمية ‎supply‎ ضمن الصنف بواسطة ‎this.supply‎ ونسند القيمة المُمررة للتابع الباني لها، والآن سنضيف تابع شراء بطاقة جديدة ‎buy()‎ حيث سيُنقص هذا التابع كمية البطاقات المتوفرة ويرسل حدثًا يجوي تفاصيل عملية الشراء كالتالي:

const EventEmitter = require("events");

class TicketManager extends EventEmitter {
    constructor(supply) {
        super();
        this.supply = supply;
    }

    buy(email, price) {
        this.supply--;
        this.emit("buy", email, price, Date.now());
    }
}

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

والآن ولكي تستطيع باقي الوحدات البرمجية استخدام هذا الصنف يجب تصديره في نهاية الملف كالتالي:

...

module.exports = TicketManager

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

الاستماع للأحداث

يمكن تسجيل مستمع إلى حدث ما باستدعاء التابع ‎on()‎ من كائن مرسل الأحداث، حيث سيستمع لحدث معين وعند إرساله سيستدعي لنا تابع رد النداء المُمرر له، وصيغة استدعاءه كالتالي:

eventEmitter.on(event_name, callback_function) {
    action
}

ملاحظة: التابع ‎on()‎ هو اسم بديل للتابع ‎addListener()‎ ضمن مرسل الأحداث ولا فرق في استخدام أي منهما، حيث سنستخدم في أمثلتنا التابع ‎on()‎ دومًا.

والآن لنبدأ بالاستماع إلى الأحداث بإنشاء ملف جافاسكربت جديد بالاسم ‎firstListener.js‎:

$ nano firstListener.js

سنختبر عملية تسجيل المستمع بطباعة رسالة ضمنه إلى الطرفية عند تلقي الحدث، ونبدأ باستيراد الصنف ‎TicketManager‎ ضمن الملف الجديد كالتالي:

const TicketManager = require("./ticketManager");

const ticketManager = new TicketManager(10);

مررنا القيمة ‎10‎ للصنف ‎TicketManager‎ كقيمة لمخزون البطاقات المتاحة، والآن لنضيف مستمع جديد لحدث الشراء ‎buy‎ كالتالي:

const TicketManager = require("./ticketManager");

const ticketManager = new TicketManager(10);

ticketManager.on("buy", () => {
    console.log("Someone bought a ticket!");
});

لإضافة مستمع جديد نستدعي التابع ‎on()‎ من الكائن ‎ticketManager‎، والمتوفر ضمن كل كائنات صنف مرسل الأحداث، وبما أن الصنف ‎TicketManager‎ يرث من صنف مرسل الأحداث الأساسي ‎EventEmitter‎ بالتالي فهذا التابع أصبح متوفرًا ضمن أي كائن من صنف مدير البطاقات ‎TicketManager‎.

نمرر تابع رد نداء للتابع ‎on()‎ كمعامل ثاني حيث ستنفذ التعليمات ضمنه عند كل إطلاق للحدث، حيث يطبع هذا التابع الرسالة ‎"Someone bought a ticket!"‎ إلى الطرفية عند كل حدث لعملية الشراء ‎buy‎.

وبعد أن سجلنا التابع كمستمع للحدث ننفذ عملية الشراء باستدعاء التابع ‎buy()‎ لينتج عنه إرسال لحدث الشراء كالتالي:

...

ticketManager.buy("test@email.com", 20);

استدعينا تابع الشراء ‎buy‎ بعنوان البريد الإلكتروني ‎test@email.com‎ وبسعر ‎20‎ لتلك للبطاقة، والآن نحفظ الملف ونخرج منه وننفذ البرنامج بتنفيذ الأمر ‎node‎ كالتالي:

$ node firstListener.js

نلاحظ ظهور الخرج:

Someone bought a ticket!

بذلك يكون مرسل الأحداث قد أرسل الحدث بنجاح وتم معالجته من قبل تابع الاستماع.

والآن لنجرب أكثر من عملية شراء ونراقب ماذا سيحدث، نفتح الملف ‎firstListener.js‎ للتعديل مجددًا ونستدعي تابع الشراء ‎buy()‎ مرة أخرى:

...

ticketManager.buy("test@email.com", 20);
ticketManager.buy("test@email.com", 20);

نحفظ الملف ونخرج منه وننفذ الملف مجددًا ونلاحظ النتيجة هذه المرة:

Someone bought a ticket!
Someone bought a ticket!

بما أن تابع الشراء ‎buy()‎ قد استُدعي مرتين فقد نتج عنه إرسال لحدث الشراء ‎buy‎ مرتين أيضًا، ثم استقبل تابع الاستماع هذين الحدثين وطبع الرسالة مرتين.

قد نحتاج في بعض الأحيان للاستماع لأول مرة يُرسَل فيها الحدث فقط وليس لكل مرة، ويمكن ذلك عبر استدعاء تابع مشابه للتابع ‎on()‎ وهو ‎once()‎ يعمل بنفس الطريقة، فهو سيسجل تابع الاستماع للحدث المحدد بالمعامل الأول، وسينفذ التابع المُمرر كمعامل ثاني له، ولكن الفرق هنا أن التابع ‎once()‎ وبعد استقبال الحدث لأول مرة سيُلغي اشتراك تابع الاستماع بالحدث ويزيله، وينفذه لمرة واحدة فقط عند أول استقبال للحدث بعد عملية التسجيل.

ولنوضح ذلك باستخدامه ضمن الملف ‎firstListener.js‎ نفتحه مجددًا للتعديل ونضيف في نهايته الشيفرة التالية لتسجيل تابع للاستماع لحدث الشراء لمرة واحدة فقط باستخدام ‎once()‎ كالتالي:

const TicketManager = require("./ticketManager");

const ticketManager = new TicketManager(10);

ticketManager.on("buy", () => {
        console.log("Someone bought a ticket!");
});

ticketManager.buy("test@email.com", 20);
ticketManager.buy("test@email.com", 20);

ticketManager.once("buy", () => {
    console.log("This is only called once");
});

نحفظ الملف ونخرج منه وننفذ البرنامج ونلاحظ الخرج التالي:

Someone bought a ticket!
Someone bought a ticket!

لا نلاحظ أي فرق هذه المرة عن الخرج السابق، والسبب أننا سجلنا مستمع للحدث بعد الانتهاء من إرسال حدث الشراء ‎buy‎ وليس قبله، لذا لم يُنفذ التابع لأنه لم يستقبل أي أحداث جديدة، أي لا يمكن الاستماع إلا للأحداث التي سترد لاحقًا بعد عملية تسجيل التابع، أما الأحداث السابقة فلا يمكن الاستماع لها، ولحل المشكلة يمكن استدعاء تابع الشراء ‎buy()‎ مرتين من جديد بعد تسجيل تابع الاستماع باستخدام ‎once()‎ لنتأكد أنه لن يُنفذ سوى لمعالجة أول حدث منها فقط:

...

ticketManager.once("buy", () => {
    console.log("This is only called once");
});

ticketManager.buy("test@email.com", 20);
ticketManager.buy("test@email.com", 20);

نحفظ الملف ونخرج منه وننفذ البرنامج لنحصل على خرج كالتالي:

Someone bought a ticket!
Someone bought a ticket!
Someone bought a ticket!
This is only called once
Someone bought a ticket!

أول رسالتين ظهرتا نتيجة أول استدعاءين لتابع الشراء ‎buy()‎ وقبل تسجيل تابع الاستماع باستخدام ‎once()‎، ولكن إضافة تابع الاستماع الجديد لا يزيل وجود التوابع المسجلة سابقًا، وستبقى تستمع للأحداث اللاحقة وتطبع تلك الرسائل، وبوجود توابع استماع تم تسجيلها باستخدام ‎on()‎ قبل تابع الاستماع الجديد الذي سجلناه باستخدام ‎once()‎، فسنلاحظ ظهور الرسالة ‎Someone bought a ticket!‎ قبل الرسالة ‎This is only called once‎، وكلا السطرين هما استجابة لحدث الشراء ‎buy‎ الثاني وما بعده.

وعند آخر استدعاء لتابع الشراء ‎buy()‎ لم يبقى ضمن مرسل الأحداث سوى التوابع التي تستمع لهذا الحدث والتي سُجلت باستخدام ‎on()‎، حيث أن التابع المستمع الذي سجلناه باستخدام ‎once()‎ أزيل تلقائيًا بعد تنفيذه لمرة واحدة فقط.

وبذلك نكون قد تعلمنا الطرق المختلفة لتسجيل توابع الاستماع للأحداث، وسنتعلم في الفقرة التالية كيف يمكننا الوصول للبيانات المرسلة مع الأحداث لمعالجتها.

استقبال بيانات الحدث

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

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

والآن لنبدأ بإنشاء وحدة خدمة إرسال البريد الإلكتروني البرمجية نُنشئ لها ملف جافاسكربت جديد ونفتحه ضمن محرر النصوص:

$ nano emailService.js

ستحوي هذه الوحدة على صنف يوفر تابع الإرسال ‎send()‎ والذي سنمرر له عنوان البريد الإلكتروني المأخوذ من بيانات حدث الشراء ‎buy‎ كالتالي:

class EmailService {
    send(email) {
        console.log(`‎Sending email to ${email}‎`);
    }
}

module.exports = EmailService

عرفنا الصنف ‎EmailService‎ الحاوي على تابع الإرسال ‎send()‎ والذي بدلًا من إرسال بريد إلكتروني حقيقي سيطبع رسالة توضح تنفيذ هذه العملية مع توضيح عنوان البريد الإلكتروني المرسل إليه.

والآن نحفظ الملف ونخرج منه ثم نُنشئ ملف جافاسكربت جديد بالاسم ‎databaseService.js‎ لوحدة خدمة قاعدة البيانات البرمجية ونفتحه ضمن محرر النصوص:

$ nano databaseService.js

سيُحاكي هذا الصنف حفظ بيانات عملية الشراء ضمن قاعدة البيانات عند استدعاء تابع الحفظ ‎save()‎ كالتالي:

class DatabaseService {
    save(email, price, timestamp) {
        console.log(`Running query: INSERT INTO orders VALUES (email, price, created) VALUES (${email}, ${price}, ${timestamp})`);
    }
}

module.exports = DatabaseService

عرفنا الصنف ‎DatabaseService‎ الحاوي على تابع الحفظ ‎save()‎ حيث سيحاكي عملية حفظ البيانات إلى قاعدة البيانات بطباعة البيانات الممررة له إلى الطرفية أيضًا، حيث سنمرر له البيانات المرفقة مع حدث الشراء ‎buy‎ وهي عنوان البريد الإلكتروني للمشتري وسعر البطاقة وتوقيت عملية الشراء.

والآن نحفظ الملف ونخرج منه ونبدأ بربط مدير البطاقات ‎TicketManager‎ مع كل من خدمتي البريد الإلكتروني ‎EmailService‎ وخدمة قاعدة البيانات ‎DatabaseService‎، حيث سنسجل تابع استماع لحدث الشراء ‎buy‎ سيستدعي داخله تابع إرسال البريد الإلكتروني ‎send()‎ وتابع حفظ البيانات في قاعدة البيانات ‎save()‎، لذا ننشئ ملف جافاسكربت الرئيسي للبرنامج ‎index.js‎ ونفتحه ضمن محرر النصوص ونبدأ باستيراد الوحدات البرمجية اللازمة:

const TicketManager = require("./ticketManager");
const EmailService = require("./emailService");
const DatabaseService = require("./databaseService");

ثم ننشئ كائنات جديدة من الأصناف السابقة، وفي هذه الخطوة سنحدد كمية قليلة للبطاقات المتاحة كالتالي:

const TicketManager = require("./ticketManager");
const EmailService = require("./emailService");
const DatabaseService = require("./databaseService");

const ticketManager = new TicketManager(3);
const emailService = new EmailService();
const databaseService = new DatabaseService();

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

const TicketManager = require("./ticketManager");
const EmailService = require("./emailService");
const DatabaseService = require("./databaseService");

const ticketManager = new TicketManager(3);
const emailService = new EmailService();
const databaseService = new DatabaseService();


ticketManager.on("buy", (email, price, timestamp) => {
    emailService.send(email);
    databaseService.save(email, price, timestamp);
});

أضفنا كما تعلمنا سابقًا تابع استماع للحدث باستخدام التابع ‎on()‎، والفرق هذه المرة أننا نقبل ثلاث معاملات ضمن تابع رد النداء تمثل البيانات المرفقة مع الحدث، ولمعرفة البيانات التي سترسل نعاين طريقة إرسال حدث الشراء داخل التابع ‎buy()‎ من صنف مدير البطاقات:

this.emit("buy", email, price, Date.now());

حيث سيقابل كل معامل نقبله ضمن تابع رد النداء معاملًا من البيانات التي نمررها لتابع إرسال الحدث السابق، فأول معامل هو البريد الإلكتروني ‎email‎ ثم السعر ‎price‎ ثم توقيت الشراء وهو التوقيت الحالي ‎Date.now()‎ والذي يقابل المعامل الأخير المسمى ‎timestamp‎ في تابع رد النداء.

وفي تابع الاستماع للحدث وعند كل إرسال لحدث الشراء ‎buy‎ سيُستدعى تابع إرسال البريد الإلكتروني ‎send()‎ من كائن الخدمة ‎emailService‎، ثم تابع حفظ البيانات ضمن قاعدة البيانات من كائن الخدمة الخاصة به ‎databaseService‎.

والآن لنختبر عملية الربط تلك كاملة باستدعاء تابع الشراء ‎buy()‎ في نهاية الملف:

...

ticketManager.buy("test@email.com", 10);

نحفظ الملف ونخرج منه، ننفذ البرنامج بتنفيذ الأمر ‎node‎ ونعاين النتيجة:

$ node index.js

نلاحظ ظهور النتيجة التالية:

Sending email to test@email.com
Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email.com, 10, 1588720081832)

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

معالجة أخطاء الأحداث

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

وحاليًا في صنف مدير البطاقات لدينا الكمية المتاحة تتناقص بمقدار واحد في كل مرة ننفذ تابع الشراء ‎buy()‎، ويمكن حاليًا تجاوز الكمية المتاحة وشراء عدد غير محدود من البطاقات، لنحل هذه المشكلة بتعديل تابع الشراء ‎buy()‎ ليرسل حدث يعبر عن خطأ في حال نفاذ الكمية المتاحة من البطاقات ومحاولة أحدهم شراء بطاقة جديدة، لذا نعود لملف مدير البطاقات ‎ticketManager.js‎ ونعدل تابع الشراء ‎buy()‎ ليصبح كالتالي:

...

buy(email, price) {
    if (this.supply > 0) {
        this.supply—;
        this.emit("buy", email, price, Date.now());
        return;
    }

    this.emit("error", new Error("There are no more tickets left to purchase"));
}
...

أضفنا العبارة الشرطية ‎if‎ لحصر عملية شراء البطاقات فقط في حال توفر كمية منها، عبر التحقق من أن الكمية الحالية أكبر من الصفر، أما في حال نفاذ الكمية سنرسل حدث الخطأ ‎error‎ ونرفق به كائن خطأ ‎Error‎ جديد يحوي وصفًا حول سبب الخطأ.

والآن نحفظ الملف ونخرج منه ونحاول الوصول لتلك الحالة من الملف الرئيسي ‎index.js‎، فحاليًا نشتري بطاقة واحدة فقط والكمية المتاحة ضمن كائن مدير البطاقات ‎ticketManager‎ هي ثلاث بطاقات فقط، لذا للوصول لحالة الخطأ يجب أن نشتري أربعة بطاقات لتجاوز الكمية المتاحة، لهذا نعود للملف ‎index.js‎ لنعدل عليه ونضيف الأسطر التالية في نهاية الملف لشراء أربعة بطاقات:

...

ticketManager.buy("test@email.com", 10);
ticketManager.buy("test@email.com", 10);
ticketManager.buy("test@email.com", 10);
ticketManager.buy("test@email.com", 10);

نحفظ الملف ونخرج منه وننفذ البرنامج:

$ node index.js

نحصل على خرج التالي:

Sending email to test@email.com
Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email.com, 10, 1588724932796)
Sending email to test@email.com
Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email.com, 10, 1588724932812)
Sending email to test@email.com
Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email.com, 10, 1588724932812)
events.js:196
      throw er; // Unhandled 'error' event
      ^

Error: There are no more tickets left to purchase
    at TicketManager.buy (/home/hassan/event-emitters/ticketManager.js:16:28)
    at Object.<anonymous> (/home/hassan/event-emitters/index.js:17:15)
    at Module._compile (internal/modules/cjs/loader.js:1128:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1167:10)
    at Module.load (internal/modules/cjs/loader.js:983:32)
    at Function.Module._load (internal/modules/cjs/loader.js:891:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
    at internal/main/run_main_module.js:17:47
Emitted 'error' event on TicketManager instance at:
    at TicketManager.buy (/home/hassan/event-emitters/ticketManager.js:16:14)
    at Object.<anonymous> (/home/hassan/event-emitters/index.js:17:15)
    [... lines matching original stack trace ...]
    at internal/main/run_main_module.js:17:47

جرى معالجة أول ثلاث أحداث شراء ‎buy‎ بنجاح بينما سبب الحدث الرابع بعد نفاذ الكمية ذلك الخطأ، لنعاين رسالة الخطأ:

...
events.js:196
      throw er; // Unhandled 'error' event
      ^

Error: There are no more tickets left to purchase
    at TicketManager.buy (/home/hassan/event-emitters/ticketManager.js:16:28)
...

توضح رسالة الخطأ نتيجة التنفيذ ونلاحظ تحديدًا رسالة الخطأ التالية:

"Unhandled 'error' event"

والتي تعني أن خطأ الحدث لم يتم معالجته، ما يعني أنه في حال أرسل مرسل الأحداث حدث الخطأ ولم نسجل أي مستمع لمعالجة هذا الحدث سيتم رمي الخطأ كما بالشكل السابق، ما يؤدي كما رأينا لتوقف تنفيذ البرنامج، لذا يفضل دومًا الاستماع لحدث الخطأ ‎error‎ من مرسل الأحداث لحل هذه المشكلة ومعالجة هذا الحدث لمنع توقف عمل البرنامج.

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

...

ticketManager.on("error", (error) => {
    console.error(`‎Gracefully handling our error: ${error}‎`);
});

ticketManager.buy("test@email.com", 10);
ticketManager.buy("test@email.com", 10);
ticketManager.buy("test@email.com", 10);
ticketManager.buy("test@email.com", 10);

نطبع داخل ذلك التابع رسالة إلى الطرفية تدل على معالجة الخطأ المُرسل باستخدام ‎console.error()‎، والآن نحفظ الملف ونخرج منه ثم نعيد تنفيذ البرنامج لنرى ما إذا كانت معالجة الخطأ ستتم بنجاح:

$ node index.js

لنحصل على الخرج التالي هذه المرة:

Sending email to test@email.com
Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email.com, 10, 1588726293332)
Sending email to test@email.com
Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email.com, 10, 1588726293348)
Sending email to test@email.com
Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email.com, 10, 1588726293348)
Gracefully handling our error: Error: There are no more tickets left to purchase

نلاحظ في آخر سطر ظهور رسالة معالجة الخطأ من قبل تابع الاستماع الذي سجلناه ولم يفشل تنفيذ البرنامج كما حدث سابقًا.

والآن وبعد أن تعلمنا طرق إرسال والاستماع للأحداث بمختلف أنواعها سنتعرف في الفقرة التالية على طرق مفيدة لإدارة توابع الاستماع للأحداث.

إدارة توابع الاستماع للأحداث

يوفر صنف مرسل الأحداث طرقًا لمراقبة عدد توابع الاستماع المشتركة بحدث ما والتحكم بها، حيث يمكن مثلًا الاستفادة من التابع ‎listenerCount()‎ لمعرفة عدد توابع الاستماع المسجلة لحدث معين ضمن الكائن، حيث يقبل ذلك التابع معامل يدل على الحدث الذي نريد معرفة عدد المستمعين له.

والآن لنعود للملف الأساسي ‎index.js‎ ونطبق ذلك حيث نزيل بدايةً استدعاءات تابع الشراء ‎buy()‎ الأربعة السابقة ثم نضيف السطرين التاليين لتصبح الشيفرة كالتالي:

const TicketManager = require("./ticketManager");
const EmailService = require("./emailService");
const DatabaseService = require("./databaseService");

const ticketManager = new TicketManager(3);
const emailService = new EmailService();
const databaseService = new DatabaseService();

ticketManager.on("buy", (email, price, timestamp) => {
    emailService.send(email);
    databaseService.save(email, price, timestamp);
});

ticketManager.on("error", (error) => {
    console.error(`Gracefully handling our error: ${error}`);
});

console.log(`We have ${ticketManager.listenerCount("buy")} listener(s) for the buy event`);
console.log(`We have ${ticketManager.listenerCount("error")} listener(s) for the error event`);

بدلًا من استدعاء تابع الشراء ‎buy()‎ نطبع إلى الطرفية سطران الأول لطباعة عدد التوابع المُستمعة لحدث الشراء ‎buy‎ باستخدام التابع ‎listenerCount()‎، والثاني لطباعة عدد التوابع المستمعة لتابع الخطأ ‎error‎، والآن نحفظ الملف ونخرج منه ثم ننفذ البرنامج مجددًا باستخدام الأمر ‎node‎ لنحصل على الخرج التالي:

We have 1 listener(s) for the buy event
We have 1 listener(s) for the error event

بما أننا استدعينا تابع التسجيل ‎on()‎ مرة واحدة لحدث الشراء ‎buy‎ ومرة واحدة أيضًا لحدث الخطأ ‎error‎ فالخرج السابق صحيح.

سنستفيد من التابع ‎listenerCount()‎ عندما نتعلم طريقة إزالة توابع الاستماع من مرسل الأحداث لنتأكد من عدم وجود مشتركين بحدث ما، فقد نحتاج أحيانًا لتسجيل تابع استماع لفترة مؤقتة فقط ثم نزيله بعد ذلك.

يمكن الاستفادة من التابع ‎off()‎ لإزالة تابع استماع من كائن مرسل الأحداث، ويقبل معاملين هما اسم الحدث وتابع الاستماع الذي نرغب بإزالته.

ملاحظة: التابع ‎off()‎ هو اسم بديل عن التابع ‎removeListener()‎ وكلاهما ينفذ نفس العملية ويقبل نفس المعاملات، وسنستخدم في أمثلتنا التابع ‎off()‎ دومًا.

وبما أنه يجب تمرير تابع الاستماع الذي نرغب بإزالته كمعامل ثانِ للتابع ‎off()‎ فيجب حفظ ذلك التابع أولًا ضمن متغير أو ثابت كي نشير إليه لاحقًا ونمرره للإزالة، فلا تصلح طريقة استخدام التوابع التي سجلناها سابقًا للأحداث ‎buy‎ و ‎error‎ للإزالة باستخدام ‎off()‎.

ولنتعرف على طريقة عمل تابع الإزالة ‎off()‎ سنضيف تابع استماع جديد ونختبر إزالته، ونبدأ بتعريف تابع رد النداء وحفظه ضمن متغير سنمرره لاحقًا لتابع الإزالة ‎off()‎، والآن نعود للملف الأساسي ‎index.js‎ ونفتحه ضمن محرر النصوص ونضيف التالي:

...

const onBuy = () => {
    console.log("I will be removed soon");
};

بعدها نسجل هذا التابع للاستماع إلى الحدث ‎buy‎ كالتالي:

...

ticketManager.on("buy", onBuy);

وللتأكد من تسجيل التابع بشكل سليم سنطبع عدد التوابع المستمعة للحدث ‎buy‎ ثم نستدعي تابع الشراء ‎buy()‎:

...

console.log(`We added a new event listener bringing our total count for the buy event to: ${ticketManager.listenerCount("buy")}`);
ticketManager.buy("test@email", 20);

نحفظ الملف ونخرج منه ونشغل البرنامج:

$ node index.js

سيظر لنا الخرج التالي:

We have 1 listener(s) for the buy event
We have 1 listener(s) for the error event
We added a new event listener bringing our total count for the buy event to: 2
Sending email to test@email
Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email, 20, 1588814306693)
I will be removed soon

نلاحظ ظهور الرسالة التي توضح عدد التوابع المستمعة لذلك الحدث، ثم استدعينا بعدها التابع ‎buy()‎ ونلاحظ تنفيذ تابعي الاستماع لذلك الحدث، حيث نفذ المستمع الأول عمليتي إرسال البريد الإلكتروني وحفظ البيانات ضمن قاعدة البيانات، ثم طبع المستمع الثاني الرسالة ‎I will be removed soon‎.

والآن لنختبر إزالة تابع الاستماع الثاني باستخدام ‎off()‎، لذا نعود للملف مجددًا ونضيف عملية الإزالة بواسطة ‎off()‎ في نهاية الملف وبعدها نطبع عدد توابع الاستماع الحالية المسجلة لنتأكد من الإزالة، ثم نختبر استدعاء تابع الشراء ‎buy()‎ مجددًا:

...

ticketManager.off("buy", onBuy);

console.log(`We now have: ${ticketManager.listenerCount("buy")} listener(s) for the buy event`);
ticketManager.buy("test@email", 20);

نلاحظ كيف مررنا المتغير ‎onBuy‎ كمعامل ثاني لتابع الإزالة ‎off()‎ لنحدد تابع الاستماع الذي نرغب بإزالته، نحفظ الملف ونخرج منه وننفذ البرنامج لمعاينة النتيجة:

$ node index.js

نلاحظ أن الخرج بقي كما كان سابقًا، وظهر سطر جديد يؤكد عملية الإزالة ويوضح عدد التوابع المسجلة، ثم بعد استدعاء تابع الشراء ‎buy()‎ نلاحظ ظهور خرج تابع الاستماع الأول فقط، بينما أُزيل تابع الاستماع الثاني:

We have 1 listener(s) for the buy event
We have 1 listener(s) for the error event
We added a new event listener bringing our total count for the buy event to: 2
Sending email to test@email
Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email, 20, 1588816352178)
I will be removed soon
We now have: 1 listener(s) for the buy event
Sending email to test@email
Running query: INSERT INTO orders VALUES (email, price, created) VALUES (test@email, 20, 1588816352178)

يمكن أيضًا إزالة كل توابع الاستماع لحدث ما دفعة واحدة باستدعاء التابع ‎removeAllListeners()‎، ونمرر له اسم الحدث الذي نرغب بإزالة التوابع التي تستمع إليه، وسنستفيد من هذا التابع لإزالة تابع الاستماع الأول للحدث ‎buy‎ الذي لم نتمكن من إزالته سابقًا بسبب طريقة تعريفه

الآن نعود للملف ‎index.js‎ ونزيل كافة توابع الاستماع باستخدام ‎removeAllListeners()‎ ثم نطبع عدد التوابع المسجلة باستخدام ‎listenerCount()‎ للتأكد من نجاح العملية، ونتحقق من ذلك أيضًا بتنفيذ عملية شراء جديدة بعد الإزالة، ونلاحظ أن لا شيء سيحدث بعد إرسال ذلك الحدث، لذا نضيف الشيفرة التالية في نهاية الملف:

...

ticketManager.removeAllListeners("buy");
console.log(`We have ${ticketManager.listenerCount("buy")} listeners for the buy event`);
ticketManager.buy("test@email", 20);
console.log("The last ticket was bought");

نحفظ الملف ونخرج منه ونشغل البرنامج:

$ node index.js

نحصل على الخرج:

...

ticketManager.removeAllListeners("buy");
console.log(`We have ${ticketManager.listenerCount("buy")} listeners for the buy event`);
ticketManager.buy("test@email", 20);
console.log("The last ticket was bought");

نلاحظ بعد إزالة كل توابع الاستماع لم يُرسل أي بريد إلكتروني ولم تُحفظ أي بيانات في قاعدة البيانات.

ختامًا

تعلمنا في هذا المقال وظيفة مرسل الأحداث وطريقة إرسال الأحداث منه باستخدام التابع ‎emit()‎ الذي يوفره الصنف ‎EventEmitter‎، ثم تعلمنا طرق الاستماع لتلك الأحداث باستخدام التابعين ‎on()‎ و ‎once()‎ لتنفيذ التعليمات البرمجية استجابة لإرسال حدث ما، وتعلمنا كيف يمكن معالجة أحداث الأخطاء، وكيفية مراقبة توابع الاستماع المسجلة باستخدام ‎listenerCount()‎، وإدارتها باستخدام التابعين ‎off()‎ و ‎removeAllListeners()‎.

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

ويمكنك الرجوع إلى توثيق نود الرسمي العربي لمرسل الأحداث للتعرف عليه أكثر.

ترجمة -وبتصرف- للمقال Using Event Emitters in Node.js لصاحبه Stack Abuse.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...