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

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

فهم "الخطبة الوحيدة" في "أنغلر"

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

How Angular Enforces singletons

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

import { Injectable } from '@angular/core';

@Injectable({
 providedIn: 'root'
})
export class MySingletonService {
 // This instance is shared application-wide
}

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

لماذا (سينغلتون) من أجل حافلة أحداث؟

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

تصميم حافلة على نطاق التطبيق

مؤسسة حافلة الحدث هي RxJS، وهي مكتبة قوية للبرمجة التفاعلية، وتعتمد الأنغلالية نفسها اعتماداً كبيراً على RxJS، مما يجعلها خياراً طبيعياً لبناء الخدمات التي تحركها الأحداث.

اختيار الموضوع الصحيح

وتوفر هذه الدراسة عدة أنواع من المواضيع:

  • Subject] — A multicast observable that can emit values to multiple participantrs. It has no initial value and only emits future events.
  • BehaviorSubject - Requires an initial value and replays the latest value to new participantrs.
  • ReplaySubject] - Replays a configurable number of previous emissions to late participantrs.
  • AsyncSubject] - لا تُعتبر سوى القيمة الأخيرة عندما يكتمل الملاحظ.

وبالنسبة لحافلة تقليدية )أحداث لا ينبغي إعادة صياغتها(، فإن الخيار السهل ]الخط الفارغ: ٧[ هو أنسب خيار، إذ أن استخدام ]النقطة ٨[ يعني أن الحدث وارد في الدولة، وهو عادة ما لا ينطبق على أحداث التطبيق مثل " تسجيل المستخدمين " أو " البقايا العازلة " ، ولكن إذا احتجتم إلى المشتركين في وقت متأخر لتلقي أحدث حدث عام)٩(.

إنشاء حدث أمني من النوع

ولمنع الأخطاء التي تحدث في الزمن، من الحكمة تحديد شكل الأحداث التي يمكن للحافلة أن تبرزها، ويمكنك إنشاء واجهة بينية لخريطة الأحداث تُعد أسماء الأحداث المحتملة وأنواع الحمولة المرتبطة بها.

export interface AppEvent {
 name: string;
 data: unknown;
}

export interface EventMap {
 'user:login': { userId: string; token: string };
 'user:logout': void;
 'data:updated': { entity: string; id: string };
 'notification:new': { message: string; type: 'success' | 'error' };
}

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

import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { EventMap } from './event-map';

@Injectable({
 providedIn: 'root'
})
export class TypedEventBusService {
 private subject = new Subject<{ name: keyof EventMap; data: EventMap[keyof EventMap] }>();

 emit<K extends keyof EventMap>(eventName: K, data: EventMap[K]): void {
 this.subject.next({ name: eventName, data });
 }

 on<K extends keyof EventMap>(eventName: K): Observable<EventMap[K]> {
 return this.subject.asObservable().pipe(
 filter((event) => event.name === eventName),
 map((event) => event.data as EventMap[K])
 );
 }
}

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

تنفيذ خدمة حافلات الأحداث الوحيدة

ونظرية قائمة، فلنبني خدمة حافلة للحوادث عملية، وإن كانت مرنة.

التنفيذ الأساسي

ابدأ بخدمة بسيطة تستخدم صيغة عامة هذه النسخة غير مطبعية ولكنها سهلة الفهم.

import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';

export interface BusEvent {
 name: string;
 data: any;
}

@Injectable({
 providedIn: 'root'
})
export class EventBusService {
 private bus = new Subject<BusEvent>();

 emit(eventName: string, data?: any): void {
 this.bus.next({ name: eventName, data });
 }

 on(eventName: string): Observable<any> {
 return this.bus.asObservable().pipe(
 filter(event => event.name === eventName),
 map(event => event.data)
 );
 }
}

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

إضافة معالجة الأخطاء

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

private errorSubject = new Subject<Error>();

emitError(error: Error): void {
 this.errorSubject.next(error);
}

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

إدارة الاشتراكات

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

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { EventBusService } from './event-bus.service';

@Component({
 selector: 'app-example',
 template: `<div>Listening for events...</div>`
})
export class ExampleComponent implements OnInit, OnDestroy {
 private subscription: Subscription;

 constructor(private eventBus: EventBusService) {}

 ngOnInit(): void {
 this.subscription = this.eventBus.on('data:updated').subscribe(data => {
 console.log('Data updated:', data);
 });
 }

 ngOnDestroy(): void {
 if (this.subscription) {
 this.subscription.unsubscribe();
 }
 }
}

ويمكنك أيضاً استخدام نمط لإلغاء الاشتراكات تلقائياً عندما يتم تدمير عنصر ما.

استخدام حافلة الأحداث في المكونات

والآن، بما أن لدينا حافلة قوية للحدث، فلننظر إليها في العمل مع سيناريو واقعي للاتصال الشامل.

مثال: نظام الإخطار

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

NotificationPublisherComponent]

import { Component } from '@angular/core';
import { EventBusService } from '../services/event-bus.service';

@Component({
 selector: 'app-notification-publisher',
 template: `
 <button (click)="sendNotification()">
 Trigger Notification
 </button>
 `
})
export class NotificationPublisherComponent {
 constructor(private eventBus: EventBusService) {}

 sendNotification(): void {
 this.eventBus.emit('notification:new', {
 message: 'Your settings have been saved.',
 type: 'success'
 });
 }
}

NottificationDisplayComponent

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { EventBusService } from '../services/event-bus.service';

@Component({
 selector: 'app-notification-display',
 template: `
 <div *ngIf="lastNotification" class="notification" [ngClass]="lastNotification.type">
 {{ lastNotification.message }}
 </div>
 `
})
export class NotificationDisplayComponent implements OnInit, OnDestroy {
 lastNotification: { message: string; type: string } | null = null;
 private subscription: Subscription;

 constructor(private eventBus: EventBusService) {}

 ngOnInit(): void {
 this.subscription = this.eventBus.on('notification:new').subscribe(data => {
 this.lastNotification = data;
 });
 }

 ngOnDestroy(): void {
 if (this.subscription) {
 this.subscription.unsubscribe();
 }
 }
}

وفي هذه المنشأة، لا يستورد كل عنصر من العنصرين الآخر أبداً، وإنما يعتمدان فقط على ما يُتقاسمه [(FLT:26]) وهذا الانقلاب المطلّق يجعل من الشفرة أسهل للحفاظ عليها واختبارها وثباتها.

استخدام حافلة الأحداث في الخدمات

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

import { Injectable, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { EventBusService } from './event-bus.service';

@Injectable({ providedIn: 'root' })
export class AuthService implements OnDestroy {
 private subscription: Subscription;

 constructor(private eventBus: EventBusService) {
 this.subscription = this.eventBus.on('user:logout').subscribe(() => {
 this.clearSession();
 });
 }

 clearSession(): void {
 // Clear tokens, navigate to login, etc.
 }

 ngOnDestroy(): void {
 this.subscription?.unsubscribe();
 }
}

Note that even though the service is a singleton, it still implements to clean up subscriptions when the application shuts down (e.g., during Angular Universal SSR or in testing).

الاعتبارات المسبقة

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

الأداء وتعبئة الموارد

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

تجنب الذاكرة

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

private destroy$ = new Subject<void>();

ngOnInit(): void {
 this.eventBus.on('data:updated')
 .pipe(takeUntil(this.destroy$))
 .subscribe(data => { ... });
}

ngOnDestroy(): void {
 this.destroy$.next();
 this.destroy$.complete();
}

الديون والتعبئة

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

emit(eventName: string, data?: any): void {
 if (environment.production === false) {
 console.debug(`[EventBus] Emitting: "${eventName}"`, data);
 }
 this.bus.next({ name: eventName, data });
}

هذه الإضافة البسيطة يمكن أن تنقذ ساعات من وقت التزيين

الوحدة التي تختبر حافلة الأحداث

اختبار حافلة الحدث نفسها مباشرة: يمكنك أن تشارك في حدث، وتصدره، وتتحقق من أن المشترك يتلقى البيانات الصحيحة.

import { TestBed } from '@angular/core/testing';
import { EventBusService } from './event-bus.service';

describe('EventBusService', () => {
 let service: EventBusService;

 beforeEach(() => {
 TestBed.configureTestingModule({});
 service = TestBed.inject(EventBusService);
 });

 it('should emit and receive events', (done) => {
 const testData = { foo: 'bar' };
 service.on('test:event').subscribe(data => {
 expect(data).toEqual(testData);
 done();
 });
 service.emit('test:event', testData);
 });

 it('should not deliver events to subscribers of different names', () => {
 const spy = jasmine.createSpy();
 service.on('other:event').subscribe(spy);
 service.emit('test:event', {});
 expect(spy).not.toHaveBeenCalled();
 });
});

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

الفوائد والخسائر المحتملة

الاستحقاقات

  • Decoupled Communication] - components and services can interact without direct references to each other, making the codebase more modular.
  • Single Source of Truth] – The singleton event bus ensures that all events flow through a single channel, streamlineing trace and debugging.
  • Reduced Memory Footprint — instead of creating multiple separate event emitters, you have exactly one instance, which is especially useful in large applications.
  • Reactive by Nature] – Leveraging RxJS allows powerful operators (debounce, combineLatest, etc.) to handle complex event flows.
  • يمكن اختباره ] - لأن حافلة الحدث هي خدمة حقنة، يمكنك بسهولة أن تسخر منها في اختبارات الوحدة.

المظاهر المحتملة

  • ]Overuse – Relying on a global event bus for every piece of communication can make the data flow hard to follow, leading to “spaghetti events”. Use it primarily for cross-cutting concerns (notifications, auth state, global settings) rather than for routine parent-child communication.
  • No Type Safety without Explicit Typing ] – If you abandoned the typed approach, you lose compilation-time safety and increase the risk of runtime crashes.
  • Memory Leaks] - كما نوقش، إن نسيان العصيان هو المصدر رقم 1 للحشرات.
  • Difficulty in Tracing Event Flow] — contrast a direct method call, the source of an event is not obvious from the event bus alone. Good naming conventions and consistent logging mitigate this.
  • Cascading Events] – Events that trigger other events can create infinite cycles if not carefully designed.

بدائل لحافلة من طراز Unton Event Bus

وفي حين أن حافلة الأحداث الوحيدة التي تُستخدم في إحدى الحالات أداة قوية، فإنها ليست الطريقة الوحيدة لإدارة الاتصالات الشاملة في الأنغلار.

  • State Management Libraries (NgRx, Akita, NGXS) - These provide a structured, Redux-like pattern for managing application state. they are overkill for small projects but excellent for large, complex applications.
  • Shared Service with BehaviorSubject — instead of a general event bus, you can create dedicated services that expose specific state as observables. This is more type-safe and avoids the “wild west” of arbitrary event names.
  • ] Compponent-Level Communication] - For closely related components, /] or a shared parent component is often simpler and more transparent.
  • Routing with queryParams or state] - Some cross-component communication can be achieved via route data.

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

خاتمة

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

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

Adopt the singleton event bus pattern where it makes sense, but avoid overusing it. Combined with Angular’s own best practices - such as modularity, reactive forms, andelli/dumb component separation - it will help you build clean, more professional Angular applications. For further reading, consult the Angular documentation on singleton services