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