Les applications angulaires modernes se développent souvent pour contenir des dizaines ou même des centaines de composants, services et modules. À mesure que la complexité augmente, le défi de gérer la communication entre les différentes parties de l'application peut aussi devenir rapidement embrouillé. Les liaisons parent-enfant / ] directes deviennent incompréhensibles, et les services qui détiennent l'état partagé peuvent rapidement devenir enchevêtrés. Une solution éprouvée à ce problème est la mise en place d'un modèle de monoton pour un bus événementiel à l'échelle de l'application.

Cet article vous permet de découvrir la base théorique du modèle de monoton, les détails de mise en œuvre étape par étape, les scénarios d'utilisation réels et les considérations avancées telles que la sécurité de type, la gestion de la mémoire et les tests. Que vous construisiez un nouveau projet ou refactoriez une base de code existante, la maîtrise de ce modèle vous permettra de concevoir des applications angulaires plus propres et évolutives.

Comprendre le modèle Singleton en angulaire

Le modèle de monoton est l'un des modèles de conception les plus utilisés en ingénierie logicielle. Son objectif principal est de limiter l'invocation d'une classe à un objet exactement et de fournir un point d'accès global à cet objet. En Angulaire, les monotones sont le plus souvent mis en œuvre via des services qui sont fournis au niveau racine.

Comment l'angulaire fait appliquer les Singletons

Lorsqu'un service est déclaré avec dans le décorateur , Angular assure que la même instance du service est partagée sur toute l'application. C'est la manière idiomatique de créer des singletons en Angular.

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

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

En utilisant , non seulement il garantit une seule instance, mais il supporte aussi le shaking des arbres. Si le service n'est jamais injecté, il peut être retiré du paquet final, réduisant ainsi l'empreinte de l'application. C'est un avantage significatif par rapport aux approches plus anciennes qui se sont appuyées sur le modèle dans les modules.

Pourquoi un Singleton pour un Event Bus?

Un bus événementiel est essentiellement un hub de message : les composants émettent des événements, et d'autres composants ou services s'abonnent à ces événements. Pour que ce modèle de communication fonctionne de manière fiable, tous les participants doivent se référer à la même instance événementiel de bus. Un monoton le garantit. Si un composant crée son propre bus événementiel privé, les événements seraient isolés et la communication se briserait.

Concevoir un bus événementiel à l'échelle de l'application

La base de notre bus événementiel est RxJS, une puissante bibliothèque pour la programmation réactive. Angular elle-même repose fortement sur RxJS, ce qui en fait un choix naturel pour la construction de services événementiels.

Choisir le bon type de sujet

RxJS fournit plusieurs types de sujets:

  • Subject – Un écran multicast observable qui peut émettre des valeurs à plusieurs abonnés. Il n'a aucune valeur initiale et n'émet que des événements futurs.
  • BehaviorSubject – Nécessite une valeur initiale et rejoue la dernière valeur aux nouveaux abonnés.
  • ReplaySubject – Rejoue un nombre configurable d'émissions antérieures aux abonnés tardifs.
  • AsyncSubject – Ne mentionne que la dernière valeur lorsque l'observable complète.

Pour un bus d'événement classique (événements à une prise qui ne devraient pas être rejoués), un simple est le choix le plus approprié. L'utilisation impliquerait que l'événement est mateful, ce qui n'est généralement pas le cas pour les événements d'application comme -utilisateur déconnecté - ou -data rafraîchi. Cependant, si vous avez besoin d'abonnés tardifs pour recevoir l'événement le plus récent, avec un tampon de taille de 1 pourrait être considéré, mais il est moins commun pour les bus d'événement générique.

Création d'un bus événementiel de type sécuritaire

Pour éviter les erreurs d'exécution, il est sage de définir la forme des événements que le bus peut émettre. Vous pouvez créer une interface de carte des événements qui énumère les noms d'événements possibles et leurs types de charge utile associés.

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' };
}

En utilisant des types mappés, vous pouvez alors créer un bus d'événements sans danger qui permet seulement d'émettre et de s'abonner à des événements connus.

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])
 );
 }
}

Cette approche offre une sécurité de compilation-temps: vous ne pouvez pas émettre un événement avec le mauvais type de charge utile, et les abonnés reçoivent correctement des données dactylographiées.

Mise en œuvre du service de bus événementiel Singleton

Avec la théorie en place, let , construit un service pratique, mais flexible, de bus événementiel.

Mise en œuvre de base

Commencez par un service simple qui utilise un générique . Cette version est non typée mais facile à comprendre.

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

Notez l'utilisation de pour empêcher les consommateurs d'appeler accidentellement à ce sujet. De plus, les opérateurs et veillent à ce que les abonnés ne reçoivent que des événements correspondant au nom spécifique.

Ajout de la gestion des erreurs

Dans une application de production, vous pouvez gérer les erreurs avec grâce. Vous pouvez utiliser un qui supporte également les notifications d'erreurs, ou vous pouvez ajouter un sujet distinct pour les erreurs.

private errorSubject = new Subject<Error>();

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

Pour plus de simplicité, nous allons garder le bus de l'événement principal concentré sur des événements, mais vous pouvez l'étendre au besoin.

Gestion des abonnements

Si un composant s'inscrit au bus événementiel mais est détruit sans s'abonner, l'abonnement reste actif et peut causer des fuites de mémoire ou des effets secondaires indésirables. Toujours stocker l'abonnement et le nettoyer dans .

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();
 }
 }
}

Vous pouvez également utiliser le modèle pour annuler automatiquement les abonnements lorsqu'un composant est détruit.

Utilisation du bus événementiel dans les composants

Maintenant que nous avons un bus d'événement robuste, voyons-le en action avec un scénario de communication multicomposants réaliste.

Exemple: Système de notification

Imaginez une application avec un composant d'en-tête (où les notifications sont affichées) et un composant de paramètres où l'utilisateur peut déclencher une notification. Ces composants sont sans rapport dans l'arborescence des composants, de sorte que les événements traditionnels ne fonctionnent pas facilement. Le bus de l'événement résout cela.

NotificationPublicateurComponent

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'
 });
 }
}

NotificationAffichageComponent

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();
 }
 }
}

Dans cette configuration, les deux composants ne s'importent jamais. Ils dépendent uniquement du partage . Ce couplage lâche facilite la maintenance, le test et le refactor.

Utilisation de l'Event Bus dans les services

Les services peuvent également s'abonner aux événements et réagir en conséquence. Par exemple, un service d'authentification pourrait écouter un événement pour effacer les jetons en cache.

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();
 }
}

Notez que même si le service est un simpleton, il met encore en œuvre pour nettoyer les abonnements lorsque l'application s'arrête (p. ex. pendant la SSR universelle angulaire ou lors des essais).

Considérations avancées

Bien qu'un bus événement monoton soit puissant, il doit être utilisé avec attention. Ci-dessous sont quelques sujets avancés que chaque développeur devrait comprendre.

Performance et dénonciation

Si les événements sont déclenchés à une fréquence élevée (p. ex., pendant le mouvement de la souris ou la diffusion en temps réel de données), envisager d'utiliser ou des opérateurs dans des composants d'abonnés. Cependant, être conscient que ces opérateurs devraient être appliqués au niveau de l'abonnement, et non à l'intérieur du bus d'événement lui-même, parce que différents abonnés peuvent avoir des besoins tarifaires différents.

Éviter les fuites de mémoire

Nous avons déjà souligné l'importance de désabonnement, mais il faut le répéter. Le bus événementiel observable est infini : il ne se termine jamais avant que le sujet ne soit désabonné manuellement ou que l'application soit détruite. Tout composant qui oublie de se désabonner provoque une fuite de mémoire. Dans les applications de grande envergure, de telles fuites peuvent accumuler et dégrader les performances au fil du temps.

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();
}

Débogue et exploitation forestière

Les systèmes de débogage par événement peuvent être difficiles car le flux des événements n'est pas immédiatement visible. Vous pouvez ajouter la connexion au bus de l'événement lui-même. Par exemple, vous pouvez enregistrer chaque événement émis en mode développement:

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

Cette simple addition peut économiser des heures de débogage.

Essai d'unité de l'Event Bus

Tester le bus d'événement lui-même est simple : vous pouvez vous abonner à un événement, l'émettre et vérifier que l'abonné reçoit les données correctes.

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();
 });
});

Lors de l'essai des composants qui utilisent le bus événementiel, injectez un espion ou une maquette de pour vérifier que le composant émet des événements corrects ou réagit correctement aux événements.

Avantages et pièges potentiels

Avantages

  • Communication découplée – Les composants et les services peuvent interagir sans référence directe les uns aux autres, rendant la base de code plus modulaire.
  • Source unique de vérité – Le bus événementiel à simpleton assure que tous les événements passent par un seul canal, simplifiant le traçage et le débogage.
  • Peinture de mémoire réduite – Au lieu de créer plusieurs émetteurs d'événements distincts, vous avez exactement une seule instance, qui est particulièrement bénéfique dans les grandes applications.
  • Réactive par Nature – Le levier RxJS permet aux opérateurs puissants (débonce, combineLatest, etc.) de gérer des flux d'événements complexes.
  • Testable – Comme le bus événement est un service injectable, vous pouvez facilement le simuler dans les tests unitaires.

Pièges potentiels

  • Surutilisation – S'appuyer sur un bus d'événements global pour chaque élément de communication peut rendre le flux de données plus difficile à suivre, conduisant à des événements --spaghetti. Utilisez-le principalement pour des préoccupations transversales (notifications, état d'auth, paramètres globaux) plutôt que pour la communication parent-enfant de routine.
  • Aucun type de sécurité sans dactylographie explicite – Si vous sautez l'approche dactylographiée, vous perdez la sécurité de compilation-temps et augmentez le risque de pannes d'exécution.
  • Mémorie fuites – Comme discuté, oublier de se désabonner est la source #1 des bogues.
  • La difficulté de tracer le flux d'événements – Contrairement à un appel de méthode directe, la source d'un événement n'est pas évidente du seul bus de l'événement.
  • Événements en cascade – Les événements qui déclenchent d'autres événements peuvent créer des boucles infinies si elles ne sont pas soigneusement conçues.

Solutions de rechange à un bus événementiel à simpleton

Bien que le bus événementiel de singleton soit un outil puissant, il n'est pas le seul moyen de gérer la communication entre composants en Angulaire.

  • Les bibliothèques de gestion d'état (NgRx, Akita, NGXS)[ – Ces bibliothèques offrent un modèle structuré, semblable à Redux pour la gestion de l'état d'application.
  • Service partagé avec BehaviorSubject – Au lieu d'un bus d'événements générique, vous pouvez créer des services dédiés qui exposent l'état spécifique comme observables. Ceci est plus sûr de type et évite le --west -wild-de noms d'événements arbitraires.
  • Communication au niveau des composants[ – Pour les composants étroitement liés, / ou un composant parent partagé est souvent plus simple et plus transparent.
  • Route avec requêteParams ou état – Une communication entre composants peut être réalisée via des données de route.

Le bus événementiel de singleton brille lorsque vous avez besoin d'un couplage libre sans bibliothèque de gestion d'état complet. Il est léger, facile à mettre en œuvre et flexible, mais il exige de la discipline.

Conclusion

La mise en place d'un modèle de monoton pour un bus événementiel à l'échelle de l'application en Angular est une technique éprouvée pour simplifier la communication entre les composants et services couplés de façon lâche. En tirant parti du système d'injection de dépendance Angular , et des flux observables puissants de RxJS, vous pouvez créer un hub d'événement centralisé et sécurisé qui rend votre application plus durable et évolutive.

Nous avons couvert la théorie derrière les singletons, nous avons traversé la construction d'un bus d'événement robuste avec des implémentations basiques et dactylographiées, nous avons démontré l'utilisation de composants croisés, et discuté de sujets avancés tels que le débogage, les tests et la gestion de la mémoire.

Adoptez le modèle de bus événement monoton là où il est logique, mais évitez de l'utiliser trop. Combiné avec Angular , ses propres meilleures pratiques – comme la modularité, les formes réactives et la séparation des composants intelligents/dumb – cela vous aidera à construire des applications angulaires plus propres et plus professionnelles. Pour plus de détails, consultez la documentation angulaire sur les services monoton, l'API RxJS Sujet, et une explication approfondie du modèle de conception monoton.