As aplicações Angular modernas crescem frequentemente para conter dezenas ou mesmo centenas de componentes, serviços e módulos. À medida que a complexidade aumenta, o desafio de gerenciar a comunicação entre diferentes partes da aplicação. As ligações pai-filho direto / ] tornam-se descomplicadas, e os serviços que mantêm o estado compartilhado podem rapidamente se tornar emaranhados. Uma solução comprovada para este problema é a implementação de um padrão de singleton para um barramento de eventos em todo o aplicativo. Ao garantir que cada componente e serviço compartilhe uma única instância de barramento de eventos centralizados, os desenvolvedores podem alcançar uma comunicação consistente, dissolvida e mantentável orientada por eventos em toda a aplicação.

Este artigo fornece um mergulho profundo na criação de um barramento de eventos singleton em Angular usando o RxJS. Vamos cobrir a base teórica do padrão singleton, detalhes de implementação passo a passo, cenários de uso do mundo real e considerações avançadas, como segurança de tipo, gerenciamento de memória e testes. Se você está construindo um novo projeto ou refactorando uma base de código legado, dominar esse padrão irá lhe capacitar a projetar aplicativos Angular mais limpos e escaláveis.

Compreender o padrão de singleton em Angular

O padrão singleton é um dos padrões de design mais utilizados na engenharia de software. Sua intenção principal é restringir a instanciação de uma classe a exatamente um objeto e fornecer um ponto global de acesso a esse objeto. Em Angular, singletons são implementados mais comumente através de serviços que são fornecidos no nível raiz.

Como a Angular Força os Singletons

O sistema de injeção de dependência (DI) do Angular é hierárquico. Quando um serviço é declarado com no decorador , o Angular garante que a mesma instância do serviço seja compartilhada em toda a aplicação. Esta é a forma idiomática de criar singletons em Angular.

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

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

Usando não só garante uma única instância, mas também suporta o tremor de árvore. Se o serviço nunca for injetado, ele pode ser removido do pacote final, reduzindo a pegada do aplicativo. Esta é uma vantagem significativa sobre as abordagens mais antigas que dependiam do padrão nos módulos.

Por que um singleton para um ônibus de evento?

Um barramento de eventos é essencialmente um hub de mensagens: componentes emitem eventos, e outros componentes ou serviços assinam esses eventos. Para que este modelo de comunicação funcione de forma confiável, todos os participantes devem se referir à mesma instância de barramento de eventos. Um únicoton garante isso. Se algum componente cria seu próprio barramento de eventos privado, os eventos seriam isolados e a comunicação quebraria. Assim, fazer do barramento de eventos um únicoton não é apenas uma conveniência; é um requisito fundamental.

Desenhando um ônibus de eventos em todo o aplicativo

A base do nosso evento é RxJS, uma poderosa biblioteca para programação reativa. Angular-se fortemente depende do RxJS, tornando-se uma escolha natural para a construção de serviços orientados para eventos.

Escolher o Tipo de Assunto Direito

O RxJS fornece vários tipos de assuntos:

  • Sujeito – Um multicast observável que pode emitir valores para vários assinantes. Não tem valor inicial e só emite eventos futuros.
  • BehaviorSubject – Requer um valor inicial e reproduz o valor mais recente para novos assinantes.
  • ReplaySubject – Reproduz um número configurável de emissões anteriores para assinantes atrasados.
  • AssyncSubject – Emite apenas o último valor quando o observável completa.

Para um ônibus de evento clássico (eventos de um tiro que não devem ser reproduzidos), uma simples é a escolha mais apropriada. Usando implicaria que o evento é de destaque, o que geralmente não é o caso de eventos de aplicação como “usuário logado para fora” ou “atualizado”. No entanto, se você precisar de assinantes atrasados para receber o evento mais recente, ] com um tamanho de buffer de 1 pode ser considerado, mas é menos comum para ônibus de eventos genéricos.

Criando um Bus de Evento Tipo-Safe

Para evitar erros de execução, é sábio definir a forma dos eventos que o barramento pode emitir. Você pode criar uma interface de mapa de eventos que enumera possíveis nomes de eventos e seus tipos de carga útil associados.

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, você pode então criar um barramento de eventos seguro de tipo que só permite emitir e subscrever eventos conhecidos.

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

Esta abordagem fornece segurança de tempo de compilação: você não pode emitir um evento com o tipo de carga útil errado, e os assinantes recebem dados digitados corretamente. Reduz drasticamente a probabilidade de erros de execução causados por estruturas de eventos desiguais.

Implementação do serviço de autocarros de evento único

Com a teoria em vigor, vamos construir um serviço de ônibus prático, mas flexível, evento.

Execução de base

Comece com um serviço simples que usa um genérico . Esta versão é não tipificada, mas 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 o uso de para impedir que os consumidores telefonem acidentalmente sobre o assunto. Além disso, os operadores e garantem que os assinantes apenas recebem eventos correspondentes ao nome específico.

Adicionando o Tratamento de Erros

Em uma aplicação de produção, você pode querer lidar com erros graciosamente. Você pode usar um que também suporta notificações de erro, ou você pode adicionar um assunto separado para erros.

private errorSubject = new Subject<Error>();

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

Para simplificar, manteremos o ônibus de eventos principais focado em apenas eventos, mas você pode expandí-lo conforme necessário.

Gerenciar Assinaturas

Um dos erros mais comuns com assinaturas observáveis é esquecer de cancelar a subscrição. Se um componente se inscrever no evento bus mas for destruído sem cancelar a inscrição, a subscrição permanece ativa e pode causar vazamentos de memória ou efeitos colaterais indesejados. Guarde sempre a subscrição e limpe-a em .

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

Você também pode usar o padrão para cancelar automaticamente as assinaturas quando um componente é destruído.

Usando o Bus de Evento em Componentes

Agora que temos um barramento de evento robusto, vamos vê-lo em ação com um cenário de comunicação realista entre componentes.

Exemplo: Sistema de Notificação

Imagine uma aplicação com um componente de cabeçalho (onde as notificações são exibidas) e um componente de configuração onde o usuário pode ativar uma notificação. Estes componentes não estão relacionados na árvore de componentes, então eventos tradicionais não funcionarão facilmente. O barramento de eventos resolve isso.

[[FLT: 0]] NotificaçãoComponente de Editor

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

[[FLT: 0]] NotificaçãoDisplayComponent

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

Nesta configuração, os dois componentes nunca se importam um ao outro. Eles só dependem do compartilhado . Este acoplamento solto torna o código mais fácil de manter, testar e refator.

Utilização do autocarro de eventos em serviços

Os serviços também podem subscrever eventos e reagir de acordo com isso. Por exemplo, um serviço de autenticação pode ouvir um evento para limpar tokens em 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();
 }
}

Note que, embora o serviço seja um singleton, ele ainda implementa para limpar assinaturas quando o aplicativo desliga (por exemplo, durante a SSR Angular Universal ou em testes).

Considerações Avançadas

Embora um barramento de evento singleton seja poderoso, ele deve ser usado com cuidado. Abaixo estão alguns tópicos avançados que todo desenvolvedor deve entender.

Desempenho e desembolso

Se os eventos forem disparados com uma alta frequência (por exemplo, durante o movimento do mouse ou a transmissão de dados em tempo real), considere usar ou operadores em componentes de assinante. No entanto, esteja ciente de que esses operadores devem ser aplicados no nível de assinatura, não dentro do próprio evento bus, porque diferentes assinantes podem ter necessidades diferentes de limitação de taxa.

Evitar Vazamento de Memória

Já realçámos a importância de não se escrever, mas é possível repetir. O evento de barramento observável é infinito: ele nunca termina até que o assunto seja descriminado manualmente ou que o aplicativo seja destruído. Qualquer componente que se esqueça de cancelar a inscrição causará uma fuga de memória. Em grandes aplicações, tais vazamentos podem acumular e degradar o desempenho ao longo do tempo. Use o padrão [[FLT: 32]] com um assunto dedicado para a suspensão.

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

Depuração e Registo

Depurar sistemas orientados para eventos pode ser um desafio porque o fluxo de eventos não é imediatamente visível. Você pode adicionar o registro no barramento de eventos em si. Por exemplo, você pode registrar todos os eventos emitidos no modo de desenvolvimento:

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

Esta adição simples pode economizar horas de tempo de depuração.

Unidade Testando o Bus de Evento

Testando o evento em si é simples: você pode se inscrever em um evento, emite-o e verificar se o assinante recebe os dados corretos.

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

Ao testar componentes que usam o barramento de eventos, injete um espião ou zombar de para verificar se o componente emite eventos corretos ou responde a eventos corretamente.

Benefícios e Potenciais Atropelamentos

Benefícios

  • Comunicação dissociada – Componentes e serviços podem interagir sem referências diretas entre si, tornando a base de códigos mais modular.
  • Fonte Única da Verdade – O barramento de eventos singleton garante que todos os eventos fluam através de um único canal, simplificando o rastreamento e depuração.
  • Pedaço de Memória Reduzida – Em vez de criar múltiplos emissores de eventos separados, você tem exatamente uma instância, que é especialmente benéfica em grandes aplicações.
  • Reactive by Nature – A alavancagem do RxJS permite que operadores poderosos (debunce, combinemÚltimas, etc.) lidem com fluxos de eventos complexos.
  • Testable – Como o ônibus de evento é um serviço injetável, você pode facilmente zombar dele em testes unitários.

Potenciais armadilhas

  • Sobreutilização – Confiar em um barramento global de eventos para cada peça de comunicação pode dificultar o fluxo de dados, levando a “eventos spaghetti”. Use-o principalmente para preocupações transversais (notificações, estado de autenticação, configurações globais) em vez de para a comunicação de rotina pai-filho.
  • No Type Safety Without Expllicit Typing – Se você pular a abordagem digitada, você perde a segurança de compilação-tempo e aumenta o risco de quebras de tempo de execução.
  • Memory Leaks – Como discutido, esquecer de cancelar a inscrição é a fonte #1 de bugs.
  • Dificilidade no Rastreamento do Fluxo de Evento – Ao contrário de uma chamada de método direta, a fonte de um evento não é óbvia do evento bus sozinho. Boas convenções de nomeação e registro consistente atenuar isso.
  • Eventos em cascata – Eventos que desencadeiam outros eventos podem criar laços infinitos se não forem cuidadosamente projetados.

Alternativas para um autocarro de evento de singleton

Embora o barramento de eventos singleton seja uma ferramenta poderosa, não é a única maneira de gerenciar a comunicação entre componentes em Angular. Considere estas alternativas:

  • Bibliotecas de Gestão do Estado (NgRx, Akita, NGXS) – Estes fornecem um padrão estruturado, semelhante ao Redux para gerenciar o estado de aplicação. Eles são exagerados para pequenos projetos, mas excelentes para aplicações grandes e complexas.
  • Serviço compartilhado com BehaviorSubject – Em vez de um barramento de eventos genérico, você pode criar serviços dedicados que expõem estado específico como observáveis. Isso é mais seguro para o tipo e evita o “oeste selvagem” de nomes de eventos arbitrários.
  • Comunicação de Nível Componente – Para componentes estreitamente relacionados, / ou um componente pai compartilhado é muitas vezes mais simples e mais transparente.
  • Routing com consultaParâmes ou estado – Alguma comunicação entre componentes pode ser alcançada através de dados de rota.

O ônibus de evento singleton brilha quando você precisa de acoplamento solto sem uma biblioteca de gerenciamento de estado completo. É leve, fácil de implementar e flexível, mas exige disciplina.

Conclusão

A implementação de um padrão singleton para um barramento de eventos em Angular é uma técnica comprovada para simplificar a comunicação em componentes e serviços acoplados de forma frouxa. Ao alavancar o sistema de injeção de dependência da Angular e os poderosos fluxos observáveis da RxJS, você pode criar um hub de eventos centralizados e seguros que torna sua aplicação mais sustentável e escalável.

Cobrimos a teoria por trás de singletons, percorremos a construção de um barramento de eventos robusto com implementações básicas e digitadas, demonstramos o uso de componentes cruzados e discutimos tópicos avançados como depuração, testes e gerenciamento de memória. As principais opções são: fornecer o serviço no nível raiz, usar um para emissão de eventos, cancelar sempre a inscrição e considerar digitar seus eventos para segurança.

Adote o padrão de barramento de evento único onde faz sentido, mas evite usá-lo em excesso. Combinado com as próprias práticas de Angular, tais como modularidade, formas reativas e separação de componentes inteligente/dumb, ele irá ajudá-lo a construir aplicativos Angular mais limpos e profissionais. Para mais leitura, consulte a Documentação Angular sobre serviços de singleton, o ] RxJS Subject API[, e uma explicação detalhada do padrão de design de singleton .