بک اند

آموزش استفاده از Event Emitter‌ها در Node.js

مقدمه رویدادها (Events) در Node.js توسط آبجکت‌هایی به نام Event Emitter (ساطع‌کننده رویداد) ایجاد می‌شوند که با ارسال یک پیام، وقوع یک عملیات را علامت‌گذاری می‌کنند. توسعه‌دهندگان جاوااسکریپت می‌توانند کدی بنویسند که به رویدادهای یک Event Emitter گوش می‌دهد (Listen)، که این به آن‌ها اجازه می‌دهد هر بار این رویدادها رخ دادند، توابع خاصی را اجرا کنند. در این زمینه، رویدادها از یک رشته شناسایی‌کننده (Identifying String) و هر داده‌ای که باید به شنوندگان (Listeners) ارسال شود، تشکیل می‌شوند.

معمولاً در Node.js، زمانی که می‌خواهیم عملی پس از تکمیل عملی دیگر انجام شود، از تکنیک‌های برنامه‌نویسی ناهمگام (Asynchronous) مانند توابع Callback تو در تو یا زنجیره‌ای کردن Promise‌ها استفاده می‌کنیم. با این حال، این تکنیک‌ها عملیات راه‌انداز و عملیات نهایی را به شدت به هم متصل (Couple) می‌کنند، که اصلاح عملیات نهایی را در آینده دشوار می‌سازد. Event Emitter‌ها راه متفاوتی برای ساختاربندی این رابطه ارائه می‌دهند: الگوی انتشار-اشتراک (Publish-Subscribe). در این الگوی معماری نرم‌افزار، یک ناشر (Publisher – همان Event Emitter) پیامی (رویدادی) را ارسال می‌کند و یک مشترک (Subscriber) رویداد را دریافت کرده و عملی را انجام می‌دهد. قدرت این الگو در این است که ناشد نیازی ندارد از مشترک‌ها اطلاع داشته باشد. ناشر پیام را منتشر می‌کند و واکنش به آن به عهده مشترک‌هاست. اگر بخواهیم رفتار اپلیکیشن خود را تغییر دهیم، می‌توانیم نحوه واکنش مشترک‌ها را بدون نیاز به تغییر ناشد تنظیم کنیم.

در این مقاله، ما یک Event Listener (شنونده رویداد) برای یک کلاس جاوااسکریپتی به نام TicketManager ایجاد می‌کنیم که به کاربر اجازه خرید بلیط می‌دهد. ما شنونده‌هایی برای رویداد buy تنظیم خواهیم کرد که هر بار یک بلیط خریداری می‌شود، فعال می‌شوند. این فرآیند همچنین نحوه مدیریت رویدادهای خطا از سمت Emitter و نحوه مدیریت مشترک‌های رویداد را نیز نشان می‌دهد.

پیش‌نیازها

  • Node.js روی ماشین توسعه شما نصب باشد. این آموزش با Node.js نسخه 20 تست شده است. برای نصب آن روی اوبونتو 24.04، می‌توانید به مستندات رسمی پارمین کلود (Parmin Cloud) مراجعه کنید.
  • آشنایی با کلاس‌های جاوااسکریپت (ES6).

مرحله ۱ — ساطع کردن (Emitting) رویدادها

در این مرحله، دو روش رایج برای ایجاد یک Event Emitter در Node.js را بررسی می‌کنیم. اولی استفاده مستقیم از آبجکت Event Emitter است و دومی ایجاد آبجکتی است که از Event Emitter ارث‌بری (Extend) می‌کند.

تصمیم‌گیری برای انتخاب یکی از این دو روش بستگی به میزان اتصال (Coupling) رویدادهای شما به عملیات آبجکت‌هایتان دارد. اگر رویدادهایی که می‌خواهید ساطع کنید نتیجه اعمال یک آبجکت هستند، احتمالاً بهتر است از کلاس Event Emitter ارث‌بری کنید تا برای راحتی کار به توابع آن دسترسی داشته باشید. اگر رویدادها مستقل از آبجکت‌های کسب‌وکار شما هستند، یک آبجکت مستقل Event Emitter ایجاد می‌کنید که توسط آبجکت‌های شما ارجاع داده می‌شود.

ابتدا یک پوشه برای ذخیره کدهای خود ایجاد کنید. در ترمینال خود، یک پوشه جدید به نام event-emitters بسازید:

bash
mkdir event-emitters
cd event-emitters

اولین فایل را با نام firstEventEmitter.js در ویرایشگر متن باز کنید:

bash
nano firstEventEmitter.js

در Node.js، ما رویدادها را از طریق کلاس EventEmitter ساطع می‌کنیم. این کلاس بخشی از ماژول events است. ابتدا این ماژول را وارد کنید:

javascript
const { EventEmitter } = require(“events”);
const firstEmitter = new EventEmitter();

حالا با اضافه کردن خط زیر، یک رویداد را ساطع کنید:

javascript
firstEmitter.emit(“My first event”);

تابع emit() برای شلیک رویدادها استفاده می‌شود. ما باید نام رویداد را به عنوان یک رشته به آن پاس دهیم. می‌توانیم هر تعداد آرگومان را بعد از نام رویداد اضافه کنیم. رویدادهایی که فقط نام دارند محدود هستند؛ آرگومان‌های دیگر به ما اجازه می‌دهند داده‌هایی را به شنوندگان بفرستیم.

فایل را ذخیره کرده و با دستور node آن را اجرا کنید:

bash
node firstEventEmitter.js

زمانی که اسکریپت اجرای خود را تمام می‌کند، هیچ خروجی در ترمینال نخواهید دید. زیرا ما هیچ پیامی را لاگ نکردیم و چیزی وجود ندارد که به رویداد ارسال شده گوش دهد. رویداد ساطع شده است، اما هیچ چیز به آن واکنش نشان نمی‌دهد.

حالا بیایید یک مثال کامل‌تر از انتشار، شنود و واکنش به رویدادها بسازیم. ما این کار را با ایجاد یک اپلیکیشن نمونه مدیریت بلیط انجام می‌دهیم. مدیریت بلیط تابعی برای خرید بلیط ارائه می‌دهد. وقتی بلیطی خریداری می‌شود، رویدادی همراه با جزئیات خریدار ارسال می‌شود.

فایل جدیدی به نام ticketManager.js ایجاد کنید:

bash
nano ticketManager.js

مانند مثال قبل، باید کلاس EventEmitter را وارد کنیم:

javascript
const EventEmitter = require(“events”);
class TicketManager extends EventEmitter {}

در اینجا، کلاس TicketManager از کلاس EventEmitter ارث‌بری می‌کند. این یعنی کلاس TicketManager متدها و ویژگی‌های EventEmitter را به ارث می‌برد و به متد emit() دسترسی پیدا می‌کند.

ما می‌خواهیم تعداد بلیط‌های اولیه را در سازنده (Constructor) تعریف کنیم:

javascript
const EventEmitter = require(“events”);
class TicketManager extends EventEmitter {
constructor(supply) {
super();
this.supply = supply;
}
}

سازنده یک آرگومان supply (موجودی) دارد. حتی اگر اعلام کردیم TicketManager یک کلاس فرزند EventEmitter است، هنوز باید super() را فراخوانی کنیم تا به متدها و ویژگی‌های EventEmitter دسترسی داشته باشیم.

حالا متد buy() را اضافه می‌کنیم:

javascript
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());
}
}
module.exports = TicketManager

در متد buy()، ایمیل خریدار و قیمت بلیط را دریافت می‌کنیم، موجودی را یکی کم کرده و یک رویداد buy را همراه با داده‌های اضافی (ایمیل، قیمت و تایم‌استمپ) ساطع می‌کنیم. در نهایت کلاس را اکسپورت (Export) می‌کنیم. فایل را ذخیره کرده و ببندید.


مرحله ۲ — گوش دادن به رویدادها

Node.js به ما اجازه می‌دهد با تابع on() یک شنونده برای یک رویداد اضافه کنیم. این متد به نام رویداد خاصی گوش می‌دهد و پس از رخ دادن رویداد، یک تابع Callback را اجرا می‌کند.

فایل جدیدی به نام firstListener.js ایجاد کنید:

bash
nano firstListener.js

ابتدا TicketManager را وارد کنید:

javascript
const TicketManager = require(“./ticketManager”);
const ticketManager = new TicketManager(10);

حالا اولین شنونده رویداد خود را برای رویداد buy اضافه می‌کنیم:

javascript
const TicketManager = require(“./ticketManager”);
const ticketManager = new TicketManager(10);
ticketManager.on(“buy”, () => {
console.log(“Someone bought a ticket!”);
});
ticketManager.buy(“test@email.com”, 20);

برای اضافه کردن شنونده جدید، از تابع on() استفاده کردیم. آرگومان دوم آن یک تابع Arrow (تابع Callback) است که پس از ساطع شدن رویداد اجرا می‌شود. سپس با فراخوانی buy() رویداد را شلیک می‌کنیم.

فایل را ذخیره و اجرا کنید:

bash
node firstListener.js

خروجی در کنسول شما این خواهد بود:

text
Output
Someone bought a ticket!

اگر متد buy() را دوبار صدا بزنید، پیام دو بار چاپ می‌شود، زیرا شنونده هر دو رویداد را دریافت می‌کند.

گاهی اوقات ما فقط به اولین باری که یک رویداد رخ می‌دهد علاقه‌مندیم. Node.js برای این مورد تابع once() را ارائه کرده است. وقتی رویدادی توسط شنونده‌ای که با once() ثبت شده دریافت می‌شود، Node.js به طور خودکار آن شنونده را حذف کرده و سپس کد را اجرا می‌کند.


مرحله ۳ — دریافت داده‌های رویداد

تا اینجا شنونده‌هایی تنظیم کردید که به رویدادهای ساطع شده واکنش نشان می‌دهند. رویدادهای ساطع شده همچنین داده‌هایی را همراه خود دارند. بیایید ببینیم چگونه می‌توانیم این داده‌ها را دریافت کنیم.

ما چند ماژول جدید Node.js ایجاد می‌کنیم: یک سرویس ایمیل و یک سرویس دیتابیس.

فایل emailService.js را ایجاد کنید:

bash
nano emailService.js
javascript
class EmailService {
send(email) {
console.log(`Sending email to ${email}`);
}
}
module.exports = EmailService

سپس فایل databaseService.js را ایجاد کنید:

bash
nano databaseService.js
javascript
class DatabaseService {
save(email, price, timestamp) {
console.log(`Running query: INSERT INTO orders VALUES (email, price, created) VALUES (${email}, ${price}, ${timestamp})`);
}
}
module.exports = DatabaseService

حالا فایل index.js را ایجاد کنید تا این ماژول‌ها را به هم متصل کند:

bash
nano index.js

ماژول‌ها را وارد کرده و آبجکت‌ها را نمونه‌سازی (Instantiate) کنید:

javascript
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.buy(“test@email.com”, 10);

در این کد، تابع Callback ما سه آرگومان دارد که با داده‌های ساطع شده از طرف emit() مطابقت دارد.

فایل را ذخیره و اجرا کنید:

bash
node index.js

خروجی زیر را مشاهده خواهید کرد:

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

داده‌ها با موفقیت دریافت شده و در توابع ما استفاده شدند.


مرحله ۴ — مدیریت رویدادهای خطا

اگر یک Event Emitter نتواند عملیات خود را انجام دهد، باید رویدادی را برای علامت‌گذاری شکست عملیات ساطع کند. در Node.js، روش استاندارد برای این کار ساطع کردن رویداد error است. این رویداد باید همراه با یک آبجکت Error باشد.

در حال حاضر، مدیریت بلیط ما با هر بار خرید، موجودی را یکی کاهش می‌دهد، اما چیزی مانع فروش بلیط بیشتر از موجودی نمی‌شود. بیایید متد buy() را طوری تغییر دهیم که اگر موجودی به 0 رسید، خطایی ساطع کند.

فایل ticketManager.js را باز کنید:

bash
nano ticketManager.js

متد buy() را به شکل زیر ویرایش کنید:

javascript
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”));
}

فایل را ذخیره کنید. حالا فایل index.js را ویرایش کنید تا 4 بلیط بخریم (در حالی که موجودی 3 است):

bash
nano index.js

به انتهای فایل خطوط زیر را اضافه کنید:

javascript
ticketManager.buy(“test@email.com”, 10);
ticketManager.buy(“test@email.com”, 10);
ticketManager.buy(“test@email.com”, 10);
ticketManager.buy(“test@email.com”, 10);

فایل را ذخیره و اجرا کنید:

bash
node index.js

برای سه خرید اول همه چیز خوب پیش می‌رود، اما در خرید چهارم برنامه کرش می‌کند:

text
Output
events.js:196
throw er; // Unhandled ‘error’ event
^
Error: There are no more tickets left to purchase

اگر یک رویداد خطا ساطع شود و شنونده‌ای برای آن ثبت نکرده باشیم، Node.js خطا را پرتاب (Throw) کرده و برنامه را کرش می‌کند. بهترین روش این است که همیشه برای رویدادهای خطا شنونده تعریف کنید.

فایل index.js را دوباره باز کنید و شنونده خطا را اضافه کنید:

javascript
ticketManager.on(“error”, (error) => {
console.error(`Gracefully handling our error: ${error}`);
});
ticketManager.buy(“test@email.com”, 10);

فایل را ذخیره و اجرا کنید. این بار به جای کرش کردن، خطا به شکل زیبایی مدیریت می‌شود:

text
Output
Sending email to test@email.com
Gracefully handling our error: Error: There are no more tickets left to purchase

مرحله ۵ — مدیریت شنوندگان رویداد

Event Emitter‌ها مکانیزم‌هایی برای مانیتورینگ و کنترل تعداد شنوندگانی که در یک رویداد مشترک شده‌اند دارند. برای دریافت تعداد شنوندگان یک رویداد، می‌توانیم از متد listenerCount() استفاده کنیم.

فایل index.js را باز کنید و تمام فراخوانی‌های buy() را پاک کنید. به جای آن کدهای زیر را اضافه کنید:

javascript
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`);

خروجی نشان می‌دهد که برای هر رویداد یک شنونده داریم.

در Node.js برای حذف یک شنونده از تابع off() استفاده می‌کنیم. این متد دو آرگومان می‌گیرد: نام رویداد و تابعی که به آن گوش می‌دهد. بنابراین برای حذف یک شنونده، تابع Callback آن باید در یک متغیر ذخیره شده باشد.

به انتهای فایل index.js اضافه کنید:

javascript
const onBuy = () => {
console.log(“I will be removed soon”);
};
ticketManager.on(“buy”, onBuy);
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);
ticketManager.off(“buy”, onBuy);
console.log(`We now have: ${ticketManager.listenerCount(“buy”)} listener(s) for the buy event`);
ticketManager.buy(“test@email”, 20);

اگر فایل را اجرا کنید، می‌بینید که پس از حذف شنونده دوم، فقط شنونده اول (ارسال ایمیل و ذخیره در دیتابیس) به رویداد واکنش نشان می‌دهد.

اگر بخواهیم تمام شنوندگان یک رویداد خاص را حذف کنیم، می‌توانیم از تابع removeAllListeners() استفاده کنیم:

javascript
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”);

پس از حذف تمام شنوندگان، دیگر هیچ واکنشی به خرید بلیط‌ها صورت نمی‌گیرد.


نتیجه‌گیری

در این آموزش، یاد گرفتید که چگونه از Event Emitter‌های Node.js برای راه‌اندازی رویدادها استفاده کنید. شما رویدادها را با emit() ساطع کردید، با on() و once() به آن‌ها گوش دادید، رویدادهای خطا را مدیریت کردید و با listenerCount() و off() شنوندگان را مانیتور و مدیریت کردید.

با استفاده از Callback‌ها و Promise‌ها، سیستم مدیریت بلیط شما باید مستقیماً با ماژول‌های ایمیل و دیتابیس ادغام می‌شد تا عملکرد یکسانی به دست آید. اما از آنجایی که از Event Emitter‌ها استفاده کردیم، رویداد از پیاده‌سازی‌های آن جدا (Decouple) شد. هر ماژولی که به مدیریت بلیط دسترسی داشته باشد، می‌تواند رویداد آن را مشاهده کرده و به آن واکنش نشان دهد. این معماری مقیاس‌پذیری (Scalability) بسیار خوبی به اپلیکیشن شما می‌بخشد.

از همراهی شما با پارمین کلود سپاسگزاریم.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا