Реализация шаблона Singleton для управления шириной приложения Event Bus в Angular
Современные угловые приложения часто становятся все более содержащими десятки или даже сотни компонентов, сервисов и модулей. По мере увеличения сложности возрастает и задача управления связью между различными частями приложения. Прямые связи между родителями и детьми / становятся громоздкими, и службы, которые удерживают общее состояние, могут быстро запутываться. Доказанным решением этой проблемы является реализация однотонного шаблона для шины событий в масштабах приложения. Обеспечивая, чтобы каждый компонент и служба делили один единый централизованный экземпляр шины событий, разработчики могут достичь последовательной, разъединенной и поддерживающей связи, управляемой событиями, по всему приложению.
Эта статья обеспечивает глубокое погружение в создание однотонной шины событий в Angular с использованием RxJS. Мы рассмотрим теоретическую основу однотонного шаблона, пошаговые детали реализации, сценарии использования в реальном мире и расширенные соображения, такие как безопасность типов, управление памятью и тестирование. Независимо от того, строите ли вы новый проект или рефакторинг устаревшей кодовой базы, освоение этого шаблона позволит вам разрабатывать более чистые, более масштабируемые приложения Angular.
Понимание шаблона Синглтона в Angular
Синглтонный паттерн является одним из наиболее широко используемых шаблонов проектирования в программной инженерии. Его основное намерение состоит в том, чтобы ограничить инстанциацию класса ровно одним объектом и обеспечить глобальную точку доступа к этому объекту. В Angular синглтоны чаще всего реализуются через сервисы, которые предоставляются на корневом уровне.
Как угловые силы синглтонов
Система впрыска зависимости Angular (DI) является иерархической. Когда услуга объявляется с в декораторе, Angular гарантирует, что один и тот же экземпляр службы используется во всем приложении. Это идиоматический способ создания одиночных узлов в Angular.
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MySingletonService {
// This instance is shared application-wide
}
Использование не только гарантирует один экземпляр, но и поддерживает дрожание деревьев. Если сервис никогда не вводится, его можно удалить из финального пакета, уменьшая отпечаток приложения. Это значительное преимущество перед более старыми подходами, которые опирались на шаблон в модулях.
Почему одиночный автобус для мероприятия?
Автобус событий по существу является центром сообщений: компоненты излучают события, а другие компоненты или службы подписываются на эти события. Для того, чтобы эта модель связи работала надежно, все участники должны ссылаться на один и тот же экземпляр автобуса событий. Синглтон гарантирует это. Если какой-либо компонент создает свой собственный частный автобус событий, события будут изолированы и связь будет нарушена. Таким образом, создание автобуса событий одиночкой - это не просто удобство; это фундаментальное требование.
Разработка автобуса событий в масштабах приложения
Основой нашего автобуса событий является RxJS, мощная библиотека для реактивного программирования. Angular в значительной степени опирается на RxJS, что делает его естественным выбором для создания сервисов, управляемых событиями.
Выбор правильного типа предмета
RxJS предлагает несколько типов предметов:
- Предмет — многоадресная обсервируемая, которая может излучать значения нескольким подписчикам. Она не имеет начального значения и излучает только будущие события.
- BehaviorSubject — Требует начального значения и переигрывает последнее значение новым подписчикам.
- ReplaySubject — Повторяет настраиваемое количество предыдущих выбросов для поздних подписчиков.
- AsyncSubject — Излучает только последнее значение, когда наблюдаемое завершается.
Для классической шины событий (однократные события, которые не должны воспроизводиться), простой является наиболее подходящим выбором. Использование будет означать, что событие является государственным, что обычно не относится к событиям приложений, таким как «пользователь вышел из системы» или «обновленные данные». Однако, если вам нужны поздние подписчики для получения последнего события, с размером буфера 1 можно рассмотреть, но это менее распространено для общих автобусов событий.
Создаем безопасный автобус
Чтобы предотвратить ошибки во время выполнения, целесообразно определить форму событий, которые может излучать шина. Можно создать интерфейс карты событий, который перечисляет возможные имена событий и связанные с ними типы полезной нагрузки.
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])
);
}
}
Такой подход обеспечивает безопасность компиляции времени: нельзя излучать событие с неправильным типом полезной нагрузки, а абоненты получают правильно набранные данные. Это резко снижает вероятность ошибок среды выполнения, вызванных несоответствующими структурами событий.
Реализация услуг автобуса Singleton Event
С этой теорией давайте построим практичный, но гибкий автобус для мероприятий.
Основное осуществление
Начните с простого сервиса, который использует общий . Эта версия не типизирована, но проста для понимания.
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();
}
}
}
Вы также можете использовать шаблон для автоматической отмены подписки при уничтожении компонента.
Использование автобуса событий в компонентах
Теперь, когда у нас есть надежная шина событий, давайте посмотрим на нее в действии с реалистичным сценарием межкомпонентной коммуникации.
Пример: Система уведомлений
Представьте себе приложение с компонентом заголовка (где отображаются уведомления) и компонентом настроек, где пользователь может запустить уведомление. Эти компоненты не связаны в дереве компонентов, поэтому традиционные события не будут работать легко.
УведомлениеИздательКомпонент
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'
});
}
}
УведомлениеДисплейКомпонент
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();
}
}
}
В этой установке два компонента никогда не импортируют друг друга. Они зависят только от общего . Эта свободная связь облегчает поддержание, тестирование и рефактор.
Использование автобуса событий в услугах
Службы также могут подписываться на события и реагировать соответствующим образом. Например, служба аутентификации может прослушивать событие для очистки кэшированных токенов.
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();
}
}
Обратите внимание, что даже если сервис является однопользовательским, он по-прежнему реализует для очистки подписок при отключении приложения (например, во время Angular Universal SSR или в тестировании).
Передовые соображения
Хотя однотонная шина событий является мощной, ее необходимо использовать продуманно. Ниже приведены некоторые продвинутые темы, которые должен понять каждый разработчик.
Исполнитель и дебоунинг
Если события активируются на высокой частоте (например, во время движения мыши или потоковой передачи данных в режиме реального времени), рассмотрите возможность использования операторов или в компонентах абонента. Однако имейте в виду, что такие операторы должны применяться на уровне подписки, а не внутри самой шины событий, поскольку разные абоненты могут иметь разные требования к ограничению скорости.
Избегать утечки памяти
Мы уже подчеркивали важность отказа от подписки, но она имеет повторяющиеся. Обозримая шина событий бесконечна: она никогда не завершается, пока субъект не отпишется вручную или приложение не будет уничтожено. Любой компонент, который забывает отказаться от подписки, вызовет утечку памяти. В больших приложениях такие утечки могут накапливаться и ухудшать производительность с течением времени. Используйте шаблон с выделенным субъектом для отказа от подписки.
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();
});
});
При тестировании компонентов, использующих шину событий, введите шпиона или макет , чтобы убедиться, что компонент излучает правильные события или реагирует на события должным образом.
Преимущества и потенциальные подводные камни
Преимущества
- Разъединенная связь — Компоненты и сервисы могут взаимодействовать без прямых ссылок друг на друга, делая кодовую базу более модульной.
- Единый источник истины — шина событий в одиночке обеспечивает прохождение всех событий по одному каналу, упрощая отслеживание и отладку.
- Сокращение следа памяти — Вместо создания нескольких отдельных излучателей событий у вас есть ровно один экземпляр, что особенно полезно в больших приложениях.
- Реактивная природа (FLT:0) — использование RxJS позволяет мощным операторам (дебютировать, комбинировать последние и т. д.) обрабатывать сложные потоки событий.
- Проверяемый — Поскольку шина событий представляет собой инъекционную услугу, вы можете легко издеваться над ней в единичных тестах.
Потенциальные подводные камни
- Чрезмерное использование — Полагаясь на глобальную шину событий для каждого элемента связи, можно затруднить отслеживание потока данных, что приведет к «событиям спагетти». Используйте его в первую очередь для межсекторальных проблем (уведомления, состояние аута, глобальные настройки), а не для обычной коммуникации между родителями и детьми.
- Без явного набора текста (FLT:1]) — Если вы пропустите типизированный подход, вы потеряете безопасность компиляции и увеличите риск сбоев во времени выполнения.
- Утечка памяти — Как обсуждалось, забывание отписаться является источником ошибок No1.
- Сложность отслеживания событийного потока — В отличие от прямого вызова метода, источник события не очевиден только из автобуса событий.
- Каскадные события — События, которые запускают другие события, могут создавать бесконечные циклы, если не тщательно продуманы.
Альтернатива однотонному автобусу
Хотя однотонная шина событий является мощным инструментом, это не единственный способ управления кросс-компонентной связью в Angular.
- Государственные библиотеки управления (NgRx, Akita, NGXS) — они обеспечивают структурированную, подобную Redux схему управления состоянием приложений. Они излишни для небольших проектов, но отлично подходят для больших, сложных приложений.
- Общий сервис с BehaviorSubject — вместо общей шины событий можно создавать выделенные сервисы, которые выставляют конкретное состояние как наблюдаемое. Это более безопасно по типу и позволяет избежать «дикого запада» произвольных названий событий.
- Коммуникация на уровне компонентов — Для тесно связанных компонентов / или общий родительский компонент часто проще и прозрачнее.
- Маршрутизация с помощью queryParams или state — Некоторая межкомпонентная связь может быть достигнута с помощью данных маршрута.
Синглтон-автобус событий сияет, когда вам нужна свободная связь без полной библиотеки управления государством. Он легкий, простой в реализации и гибкий, но требует дисциплины.
Заключение
Внедрение однотонного шаблона для шины событий в Angular - это проверенная техника для упрощения связи между слабо связанными компонентами и службами. Используя систему впрыска зависимости Angular и мощные наблюдаемые потоки RxJS, вы можете создать централизованный, безопасный для типа центр событий, который делает ваше приложение более удобным и масштабируемым.
Мы рассмотрели теорию, стоящую за одиночными, прошли через создание надежной шины событий с базовыми и типизированными реализациями, продемонстрировали кросс-компонентное использование и обсудили передовые темы, такие как отладка, тестирование и управление памятью. Ключевыми выводами являются: предоставить услугу на уровне корня, использовать для излучения событий, всегда отписываться и рассмотреть возможность ввода ваших событий для безопасности.
Примите шаблон однотонной шины событий, где это имеет смысл, но избегайте его чрезмерного использования. В сочетании с собственными лучшими практиками Angular, такими как модульность, реактивные формы и разделение компонентов Smart / Dumb, это поможет вам создать более чистые, более профессиональные приложения Angular. Для дальнейшего чтения обратитесь к Угловая документация по однотонным услугам , RxJS Subject API и подробное объяснение шаблона проектирования синглетона .