Le applicazioni moderne angolari spesso crescono per contenere decine o anche centinaia di componenti, servizi e moduli. Come aumenta la complessità, così la sfida di gestire la comunicazione tra diverse parti dell'applicazione.

Questo articolo fornisce un'immersione profonda nella creazione di un bus di eventi singleton in Angular utilizzando RxJS. Copriremo la base teorica del modello singleton, dettagli di implementazione passo per passo, scenari di utilizzo del mondo reale, e considerazioni avanzate come la sicurezza del tipo, la gestione della memoria e il test.

Comprendere il modello Singleton in angolare

Il modello singleton è uno dei modelli di progettazione più utilizzati nell'ingegneria del software, il cui scopo principale è quello di limitare l'istantazione di una classe ad un oggetto esattamente e di fornire un punto di accesso globale a tale oggetto.

Come le forze angolari Singletons

Il sistema di iniezione di dipendenza (DI) di Angular è gerarchico: quando un servizio viene dichiarato con [] nell'arredatore [], Angular assicura che la stessa istanza del servizio sia condivisa in tutta l'applicazione.

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

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

Se il servizio non viene mai iniettato, può essere rimosso dal fascio finale, riducendo l’impronta dell’applicazione. Questo è un vantaggio significativo rispetto agli approcci più vecchi che si basano sul modello nei moduli.

Perché un Singleton per un bus per eventi?

Un bus di eventi è essenzialmente un hub di messaggi: componenti emettono eventi, e altri componenti o servizi abbonarsi a tali eventi. Per questo modello di comunicazione lavorare in modo affidabile, tutti i partecipanti devono fare riferimento alla stessa istanza di bus di eventi. Un singleton lo garantisce. Se qualsiasi componente crea il proprio bus di eventi privati, gli eventi sarebbero isolati e la comunicazione si romperebbe.

Progettazione di un bus di eventi a livello di applicazione

Il nostro bus di eventi è RxJS, una potente biblioteca per la programmazione reattiva, che si basa fortemente su RxJS, rendendolo una scelta naturale per la costruzione di servizi organizzativi.

Scegliere il tipo di soggetto destro

RxJS fornisce diversi tipi di soggetti:

  • Subject[] – Un multicast osservabile che può emettere valori a più iscritti.
  • BehaviorSubject[] – Richiede un valore iniziale e rielabora l'ultimo valore ai nuovi iscritti.
  • ReplaySubject[] – Riproduce un numero configurabile di emissioni precedenti agli abbonati tardivi.
  • AsyncSubject[] – Emette solo l'ultimo valore quando l'osservabile completa.

Per un classico bus di eventi (un colpo eventi che non dovrebbero essere riprodotti), una pianura è la scelta più appropriata. Utilizzando implica che l'evento è statoso, che di solito non è il caso per gli eventi di applicazione come “utente registrato” o “dati rinfrescati”. Tuttavia, se avete bisogno di abbonati tardi per ricevere l'evento più recente, potrebbe essere considerato un buffer di dimensioni meno generico

Creazione di un bus di eventi tipo-Safe

Per evitare errori di runtime, è saggio definire la forma degli eventi che il bus può emettere. È possibile creare un'interfaccia di mappa eventi che enumera i nomi possibili degli eventi e i loro tipi di payload associati.

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

Utilizzando i tipi mappati, è possibile creare un bus di eventi tipo sicuro che consente solo di emettere e sottoscrivere eventi noti.

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

Questo approccio fornisce sicurezza di compilazione: non è possibile emettere un evento con il tipo di carico errato, e gli abbonati ricevono dati digitati correttamente.

Implementare il servizio bus per eventi Singleton

Con la teoria in atto, costruiamo un servizio pratico, ma flessibile, di bus di eventi.

Attuazione di base

Inizia con un servizio semplice che utilizza un generico . Questa versione è non di tipo ma facile da capire.

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

Si noti l'uso di per impedire ai consumatori di chiamare accidentalmente [ sul soggetto. Inoltre, gli operatori [ e [ assicurano che gli abbonati ricevano solo eventi corrispondenti al nome specifico.

Aggiungere la gestione degli errori

In un'applicazione di produzione, si potrebbe desiderare di gestire gli errori con grazia. È possibile utilizzare un che supporta anche le notifiche di errore, o è possibile aggiungere un soggetto separato per gli errori.

private errorSubject = new Subject<Error>();

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

Per semplicità, terremo il bus principale dell'evento focalizzato su eventi giusti, ma si può espandere come necessario.

Gestione delle Abbonamenti

Se un componente si iscrive all'autobus dell'evento ma viene distrutto senza sottoscrizioni, l'abbonamento rimane attivo e può causare perdite di memoria o effetti collaterali indesiderati. Conservare sempre l'abbonamento e pulirlo in .

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

È inoltre possibile utilizzare il modello ] per annullare automaticamente gli abbonamenti quando un componente viene distrutto.

Utilizzo del bus eventi in componenti

Ora che abbiamo un bus di eventi robusto, vediamolo in azione con uno scenario di comunicazione cross-component realistico.

Esempio: Sistema di notifica

Immaginate un'applicazione con un componente dell'intestazione (dove vengono visualizzate le notifiche) e un componente delle impostazioni in cui l'utente può attivare una notifica. Questi componenti non sono correlati all'albero dei componenti, quindi gli eventi tradizionali non funzionano facilmente.

Component di Notifica Editore[]

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

NotificationDisplayComponent[]

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

In questa configurazione, i due componenti non si importano mai a vicenda, ma dipendono solo dalla condivisione []. Questo accoppiamento sciolto rende il codice più facile da mantenere, testare e refactor.

Utilizzo dell'autobus per eventi nei servizi

I servizi possono anche iscriversi agli eventi e reagire di conseguenza. Ad esempio, un servizio di autenticazione potrebbe ascoltare un evento per cancellare i token 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();
 }
}

Si noti che anche se il servizio è un singleton, implementa ancora [] per pulire gli abbonamenti quando l'applicazione si spegne (ad esempio, durante la SSR universale angolare o in prova).

Considerazioni avanzate

Mentre un bus di eventi singleton è potente, deve essere utilizzato con pensieri. Di seguito sono alcuni argomenti avanzati che ogni sviluppatore dovrebbe capire.

Prestazioni e rimbalzo

Se gli eventi vengono licenziati ad alta frequenza (ad esempio, durante il movimento del mouse o lo streaming di dati in tempo reale), prendere in considerazione l'utilizzo o [ degli operatori nei componenti dell'abbonato. Tuttavia, essere consapevoli che tali operatori dovrebbero essere applicati a livello di abbonamento, non all'interno dell'autobus stesso evento, perché i diversi abbonati possono avere diverse esigenze di limitazione della tariffa.

Evitare le perdite di memoria

Abbiamo già sottolineato l'importanza di snodificare, ma porta a ripetere. L'autobus dell'evento osservabile è infinito: non si completa mai fino a quando il soggetto non è sottoscritto manualmente o l'applicazione viene distrutta. Qualsiasi componente che dimentica di annullare l'iscrizione causerà una perdita di memoria. In grandi applicazioni, tali perdite possono accumulare e degradare le prestazioni nel tempo.

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

Debug e Logging

I sistemi di debug basati su eventi possono essere impegnativi perché il flusso degli eventi non è immediatamente visibile. È possibile aggiungere il login all'autobus dell'evento stesso. Ad esempio, è possibile registrare ogni evento emesso in modalità di sviluppo:

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

Questa semplice aggiunta può risparmiare ore di tempo di debug.

Unità di prova dell'autobus dell'evento

Testare l'autobus dell'evento è semplice: è possibile iscriversi a un evento, emettere e verificare che l'abbonato riceva i dati corretti.

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

Quando si verificano componenti che utilizzano l'autobus dell'evento, iniettano una spia o un mock di ] per verificare che il componente emette eventi corretti o risponde correttamente agli eventi.

Vantaggi e potenziali cadute

Vantaggi

  • Comunicazione codificata[[] – Componenti e servizi possono interagire senza riferimenti diretti l'uno all'altro, rendendo la base di codice più modulare.
  • Single Source of Truth[[] – L'autobus per eventi singleton assicura che tutti gli eventi fluiscano attraverso un unico canale, semplificando il tracciamento e il debugging.
  • Ridotto Memoria Footprint[] – Invece di creare più emettitori di eventi separati, hai esattamente un'istanza, che è particolarmente utile nelle grandi applicazioni.
  • Riattiva dalla natura[[] – L'ipercentuale RxJS consente ai potenti operatori (debounce, combinaLatest, ecc.) di gestire flussi complessi di eventi.
  • Testable[] – Poiché l'autobus dell'evento è un servizio iniettabile, è possibile facilmente mock it in test unità.

Potenziali cadute

  • Overuse[[] – Il ripiegamento su un bus di eventi globale per ogni pezzo di comunicazione può rendere più difficile il flusso dei dati da seguire, portando a “sperghetti event”.
  • Nessun tipo di sicurezza senza la digitazione esplicita[[] – Se si salta l'approccio digitato, si perde la sicurezza di compilazione e aumenta il rischio di crash runtime.
  • Leaks di memoria[] – Come discusso, dimenticare di annullare l'iscrizione è la fonte #1 di bug.
  • Difficoltà nel tracciare il flusso degli eventi[[] – A differenza di una chiamata diretta metodo, la fonte di un evento non è evidente solo dal bus dell'evento.
  • Cascading Events[[] – Gli eventi che innescano altri eventi possono creare loop infinite se non accuratamente progettati.

Alternative a un singolo evento Bus

Mentre l'autobus per eventi singleton è uno strumento potente, non è l'unico modo per gestire la comunicazione cross-component in Angular.

  • Le biblioteche di gestione dello stato (NgRx, Akita, NGXS)] – Questi forniscono un modello strutturato e simile a Redux per la gestione dello stato di applicazione. Sono overkill per piccoli progetti ma eccellenti per applicazioni complesse e grandi.
  • Servizio condiviso con BehaviorSubject[[] – Invece di un bus di eventi generico, è possibile creare servizi dedicati che espongono lo stato specifico come osservabili.
  • Comunicazione completa [[] – Per componenti strettamente correlati []]/[]] o un componente genitore condiviso è spesso più semplice e trasparente.
  • Immissione con queryParams o stato[] – Alcune comunicazioni intercomponenti possono essere raggiunte tramite dati di percorso.

L'autobus singleton è illuminato da un accoppiamento sciolto senza una libreria di gestione dello stato, leggera, facile da implementare e flessibile, ma richiede disciplina.

Conclusioni

Implementare un modello singleton per un bus di eventi a livello di applicazione in Angular è una tecnica comprovata per semplificare la comunicazione tra componenti e servizi accoppiati allentatamente. Levando il sistema di iniezione di dipendenza di Angular e i potenti flussi osservabili di RxJS, è possibile creare un hub eventi centralizzato e sicuro che rende la vostra applicazione più mantenibile e scalabile.

Abbiamo coperto la teoria dietro singletons, camminato attraverso la costruzione di un robusto bus di eventi con implementazioni di base e digitate, dimostrato l'uso cross-component, e discusso argomenti avanzati come debugging, test e gestione della memoria. I takeaway chiave sono: fornire il servizio a livello di root, utilizzare un per l'emissione di eventi, sempre annullare l'iscrizione, e considerare di digitare i vostri eventi per la sicurezza.

Adottando il modello di bus di eventi singleton in cui ha senso, ma evitare di sovraccaricarlo. Combinato con le proprie migliori pratiche di Angular, come la modularità, le forme reattive e la separazione dei componenti intelligenti/dumb, ti aiuterà a costruire applicazioni angolari più pulite e professionali.