Table of Contents
Las aplicaciones modernas Angulares suelen crecer para contener docenas o incluso cientos de componentes, servicios y módulos. A medida que aumenta la complejidad, también el desafío de gestionar la comunicación entre diferentes partes de la aplicación.Padre directo hijo / ] los enlaces se vuelven inmutiles, y los servicios que mantienen estado compartido pueden rápidamente enredarse.
Este artículo proporciona una profunda inmersión en la creación de un bus de eventos de singleton en Angular utilizando RxJS. Cubriremos la base teórica del patrón de singleton, detalles de implementación paso a paso, escenarios de uso real y consideraciones avanzadas como seguridad de tipo, gestión de memoria y pruebas. Si usted está construyendo un nuevo proyecto o refactoring a una base de código heredada, dominar este patrón le permitirá diseñar aplicaciones anulares más limpias.
Comprender el patrón de Singleton en Angular
El patrón de singleton es uno de los patrones de diseño más utilizados en la ingeniería de software. Su objetivo principal es restringir la instantánea de una clase a exactamente un objeto y proporcionar un punto de acceso global a ese objeto. En Angular, los singletons se implementan más comúnmente a través de servicios que se proporcionan a nivel de raíz.
Cómo fuerzas anulares Singletons
El sistema de inyección de dependencia de Angular (DI) es jerárquico. Cuando se declara un servicio con en el decorador , Angular asegura que el mismo caso del servicio se comparte a través de toda la aplicación. Esta es la forma idiomática de crear singletons en Angular.
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MySingletonService {
// This instance is shared application-wide
}
Utilizando no sólo garantiza una sola instancia sino que también soporta el arbolado. Si el servicio nunca se inyecta, puede ser eliminado del paquete final, reduciendo la huella de la aplicación. Esta es una ventaja significativa sobre los enfoques más antiguos que se basa en el patrón en los módulos.
¿Por qué un Singleton para un autobús de eventos?
Un autobús de eventos es esencialmente un centro de mensajes: los componentes emiten eventos, y otros componentes o servicios se suscriben a esos eventos. Para que este modelo de comunicación funcione de forma fiable, todos los participantes deben referirse a la misma instancia de autobús de eventos. Un singleton garantiza esto. Si cualquier componente crea su propio bus de eventos privados, los eventos estarían aislados y la comunicación se rompería. Por lo tanto, hacer el autobús de evento un singleton no es sólo una comodidad; es un requisito fundamental.
Diseño de un autobús de eventos a nivel de toda la aplicación
La base de nuestro bus de eventos es RxJS, una poderosa biblioteca para la programación reactiva. Angular en sí misma depende en gran medida de RxJS, lo que lo convierte en una opción natural para la construcción de servicios impulsados por eventos.
Elegir el tipo de asunto correcto
RxJS ofrece varios tipos de temas:
- Subject] – Un observable multicast que puede emitir valores a múltiples suscriptores. No tiene valor inicial y sólo emite eventos futuros.
- ComportamientoSubjeto – Requiere un valor inicial y vuelve a reproducir el valor más reciente a los nuevos suscriptores.
- ReplaySubject] – Replays a configurable number of previous emissions to late subscribers.
- AsyncSubject – Emite sólo el último valor cuando el observable completa.
Para un clásico de los eventos de un evento (eventos de un solo disparo que no deben ser repetidas), una simple es la opción más adecuada. Utilizar implicaría que el evento es apasionado, que generalmente no es el caso de eventos de aplicación como “usuario conectado” o “datos refrescados”. Sin embargo, si necesita abonados tardientes para recibir el evento más reciente, [FLT: ]
Crear un autobús de eventos tipo-salva
Para evitar errores de tiempo de ejecución, es prudente definir la forma de eventos que el autobús puede emitir. Puede crear una interfaz de mapa de eventos que enumera los posibles nombres de eventos y sus tipos de carga de pago asociados.
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' };
}
Usando tipos mapeados, puede crear un bus de eventos de tipo seguro que solo permite emitir y suscribir eventos conocidos.
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])
);
}
}
Este enfoque proporciona seguridad en tiempo de compilación: no puede emitir un evento con el tipo de carga de pago incorrecto, y los suscriptores reciben datos correctamente escritos. Reduce dramáticamente la probabilidad de errores de tiempo de ejecución causados por estructuras de eventos desajustadas.
Implementación del Servicio de Autobús de Evento Único
Con la teoría en su lugar, vamos a construir un servicio de autobús práctico, pero flexible, de eventos.
Aplicación básica
Comience con un servicio simple que utiliza un genérico . Esta versión no está escrita pero fácil de entender.
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)
);
}
}
Observe el uso de para evitar que los consumidores llamen accidentalmente ] sobre el tema. Además, los y operadores aseguran que los suscriptores sólo reciban eventos que coincidan con el nombre específico.
Añadiendo el manejo de errores
En una aplicación de producción, usted puede querer manejar los errores con gracia. Puede utilizar un que también admite notificaciones de errores, o puede añadir un sujeto separado para errores.
private errorSubject = new Subject<Error>();
emitError(error: Error): void {
this.errorSubject.next(error);
}
Para la simplicidad, mantendremos el autobús central del evento centrado en eventos justos, pero usted puede ampliarlo según sea necesario.
Gestión de las Suscripciones
Uno de los errores más comunes con las suscripciones observables se olvida de darse de baja. Si un componente se suscribe al autobús del evento pero se destruye sin darse de baja, la suscripción sigue activa y puede causar fugas de memoria o efectos secundarios no deseados. Siempre guarda la suscripción y limpie en .
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();
}
}
}
También puede utilizar el patrón para cancelar automáticamente las suscripciones cuando se destruye un componente.
Utilizando el Bus de Evento en Componentes
Ahora que tenemos un bus de eventos robusto, veamos en acción con un escenario de comunicación realista entre componentes.
Ejemplo: Sistema de notificación
Imagine una aplicación con un componente de encabezado (donde se muestran las notificaciones) y un componente de configuración donde el usuario puede activar una notificación. Estos componentes no están relacionados en el árbol de componentes, por lo que los eventos tradicionales no funcionarán fácilmente.
NotificaciónPublisherComponent
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'
});
}
}
NotificaciónDisplayComponent
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();
}
}
}
En esta configuración, los dos componentes nunca se importan. Sólo dependen de la combinación . Este acoplamiento suelto facilita el mantenimiento, la prueba y el refactor.
Usando el Bus de Eventos en Servicios
Los servicios también pueden suscribirse a eventos y reaccionar en consecuencia. Por ejemplo, un servicio de autenticación podría escuchar un evento para obtener fichas de caché claras.
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();
}
}
Tenga en cuenta que aunque el servicio es un singleton, todavía implementa para limpiar las suscripciones cuando la aplicación se cierra (por ejemplo, durante la SSR universal anular o en pruebas).
Consideraciones avanzadas
Mientras que un autobús de evento de singleton es poderoso, debe ser utilizado con reflexión. A continuación se presentan algunos temas avanzados que cada desarrollador debe entender.
Performance and Debouncing
Si los eventos se disparan a una alta frecuencia (por ejemplo, durante el movimiento del ratón o la transmisión de datos en tiempo real), considere utilizar o ] operadores en componentes del suscriptor. Sin embargo, tenga en cuenta que tales operadores deben ser aplicados a nivel de suscripción, no dentro del propio bus de evento, porque diferentes suscriptores pueden tener diferentes necesidades de limitación de tarifas.
Evitar los Líderes de memoria
Ya hemos subrayado la importancia de la suscripción, pero lleva repetindo. El evento bus observable es infinito: nunca se completa hasta que el sujeto no se suscribe manualmente o la aplicación se destruye. Cualquier componente que se olvide de darse de baja causará una fuga de memoria. En aplicaciones grandes, tales fugas pueden acumular y degradar el rendimiento con el tiempo. Utilice el patrón con un sujeto dedicado para la suscripción.
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();
}
Debugging y Logging
Los sistemas de depuración de eventos pueden ser difíciles porque el flujo de eventos no es inmediatamente visible. Puede añadir la conexión al bus de eventos en sí. Por ejemplo, podría registrar cada evento emitido en modo de desarrollo:
emit(eventName: string, data?: any): void {
if (environment.production === false) {
console.debug(`[EventBus] Emitting: "${eventName}"`, data);
}
this.bus.next({ name: eventName, data });
}
Esta simple adición puede ahorrar horas de tiempo de depuración.
Unidad de prueba del autobús del evento
Probando el autobús del evento en sí es directo: puede suscribirse a un evento, emitirlo y comprobar que el suscriptor recibe los datos correctos.
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();
});
});
Cuando los componentes de prueba que utilizan el autobús del evento, inyecta un espía o mock de para verificar que el componente emite eventos correctos o responde a eventos correctamente.
Beneficios y posibles caídas
Beneficios
- Comunicación desacoplada – Los componentes y servicios pueden interactuar sin referencias directas entre sí, haciendo que la base de código sea más modular.
- Fuente única de la Verdad] – El autobús de eventos de un solo botón garantiza que todos los eventos fluyan a través de un solo canal, simplificando el rastreo y depurando.
- Reduced Memory Footprint – En lugar de crear múltiples emisores de eventos separados, usted tiene exactamente una instancia, que es especialmente beneficiosa en aplicaciones grandes.
- Reactivo por la Naturaleza – Promedio RxJS permite a los operadores poderosos (debounce, combineLatest, etc.) manejar flujos complejos de eventos.
- Testable – Debido a que el autobús de eventos es un servicio inyectable, se puede burlar fácilmente en pruebas de unidad.
Potential Pitfalls
- Overuse] – Resistir en un autobús de eventos global para cada pieza de comunicación puede hacer que el flujo de datos sea más difícil de seguir, lo que lleva a “eventos de espaguetis”. Úsalo principalmente para preocupaciones transversales (notificaciones, estado de austeridad, escenarios globales) en lugar de para la comunicación rutinaria entre padres e hijos.
- No Tipo Seguridad Sin Tipo Titulación – Si se salta el enfoque tipo, se pierde la seguridad del tiempo de compilación y aumenta el riesgo de accidentes de tiempo de ejecución.
- Líderes de memoria – Como se ha discutido, olvidarse de darse de baja es la fuente #1 de errores.
- Dificultad en Tracing Event Flow – A diferencia de una llamada directa de método, la fuente de un evento no es obvia solamente del autobús del evento. Buenas convenciones de nombres y consistentes logging mitiga esto.
- Eventos de cata – Eventos que desencadenan otros eventos pueden crear bucles infinitos si no están diseñados cuidadosamente.
Alternativas a un autobús de un evento de Singleton
Mientras que el autobús de eventos singleton es una herramienta poderosa, no es la única manera de gestionar la comunicación intercomponente en Angular. Considere estas alternativas:
- Bibliotecas de Gestión Estatal (NgRx, Akita, NGXS): Estas proporcionan un patrón estructurado, similar a Redux para gestionar el estado de aplicación. Son demasiados para proyectos pequeños pero excelentes para aplicaciones grandes y complejas.
- Servicio compartido con BehaviorSubject – En lugar de un autobús de eventos genérico, puede crear servicios dedicados que expongan estado específico como observables. Esto es más seguro de tipo y evita el "roeste salvaje" de nombres de eventos arbitrarios.
- Comunicación Componente-Level] – Para componentes estrechamente relacionados, /] o un componente de padre compartido es a menudo más simple y transparente.
- Resurrando con Params de consulta o estado] – Se puede lograr una comunicación intercomponente a través de datos de ruta.
El autobús de evento de singleton brilla cuando se necesita acoplamiento suelto sin una biblioteca de gestión estatal completa. Es ligero, fácil de implementar y flexible, pero exige disciplina.
Conclusión
Implementar un patrón de singleton para un bus de eventos en Angular es una técnica probada para simplificar la comunicación entre componentes y servicios acoplados. Aprovechando el sistema de inyección de dependencia de Angular y las potentes corrientes observables de RxJS, puedes crear un centro de eventos centralizado y seguro de tipo que hace que tu aplicación sea más sostenible y escalable.
Cubrimos la teoría detrás de singletons, caminamos a través de la construcción de un robusto autobús de eventos con implementaciones básicas y tipo, uso transversal demostrado, y discutimos temas avanzados como depuración, pruebas y gestión de memoria. Los principales usuarios son: proporcionar el servicio a nivel de raíz, utilizar un para emisión de eventos, siempre darse de baja, y considerar la posibilidad de atar sus eventos para seguridad.
Adoptar el patrón de bus de un soloton donde tenga sentido, pero evitar sobreponerlo. Combinado con las propias prácticas de Angular, como la modularidad, las formas reactivas y la separación de componentes inteligentes/dembados, le ayudará a construir aplicaciones angloulares más limpias y profesionales. Para más lectura, consulte el [FLT1] [Lámenes de la API]