Описание проблемы
Если вы разрабатывали приложения на Angular, то наверняка сталкивались с ситуацией, когда множество компонентов требуют тесного взаимодействия друг с другом. Например:
Один компонент должен отправлять события другому компоненту (или нескольким), что часто приводит к написанию громоздкого и запутанного кода.
Использование Input/Output связей может подходить для простых случаев, но становится затруднительным в масштабируемых приложениях.
Проблемы с утечками при работе с RxJS Subjects или Event Emitters — нужно следить за отключением компонентов/сервисов от подписок в конце жизненного цикла.
Код становится сильно связанным (tight coupling), что затрудняет поддержку, тестирование и рефакторинг.
Рассмотрим пример: в приложении имеется два компонента (отправитель и получатель), которые должны взаимодействовать. Часто это выглядит примерно так:
// Sender.component.ts
import { EventEmitter, Output } from '@angular/core';
@Component({
selector: 'app-sender',
template: `<button (click)="sendEvent()">Отправить</button>`,
})
export class SenderComponent {
@Output() eventFromSender = new EventEmitter<string>();
sendEvent() {
this.eventFromSender.emit('Событие от отправителя!');
}
}
// Receiver.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-receiver',
template: `<p>{{ message }}</p>`,
})
export class ReceiverComponent {
@Input() message = '';
}
Соединение этих компонентов происходит на уровне родителя:
<!-- Parent.component.html -->
<app-sender (eventFromSender)="handleEvent($event)"></app-sender>
<app-receiver [message]="message"></app-receiver>
// Parent.component.ts
export class ParentComponent {
message = '';
handleEvent(event: string) {
this.message = event;
}
}
На первый взгляд это просто, но если количество таких взаимодействий между компонентами растёт, либо они начинают находиться в разных модулях, ваш код быстро превращается в клубок событий и зависимостей. Сплошные @Input, @Output, Services и Subjects — это как минимум неудобно.
Как помогает @artstesh/postboy
Библиотека @artstesh/postboy создана, чтобы:
Снизить связанность кода — больше никаких прямых связей компонентов через Input/Output или событийные Subjects.
Упростить архитектуру — теперь каждый компонент может "общаться" с другими через глобальный механизм событий, не зная детали их реализации.
Минимизировать зависимости — лёгкий инструмент, который не требует громоздких библиотек или решений.
@artstesh/postboy реализует гибкий и понятный интерфейс для обмена событиями. Вам достаточно "подписаться" на нужное событие и "отправить" его, когда это потребуется. Компоненты не знают друг о друге, но взаимодействуют абсолютно прозрачно.
Теперь посмотрим, как можно упростить предыдущий пример с помощью @artstesh/postboy.
1. Установка библиотеки
Во-первых, добавьте библиотеку в свой проект:
npm install @artstesh/postboy@2
Первоначальная установка потребует от нас создания нескольких дополнительных классов, отвечающих за управление событиями по всему приложению.
Центральный сервис, наследующий PostboyService, расположим его где-нибудь ближе к корню проекта, в папке services:
// src/app/services/app-postboy.service.ts
import {PostboyService} from '@artstesh/postboy';
@Injectable({providedIn: 'root'})
export class AppPostboyService extends PostboyService{}
Сервис регистрации событий, отвечающий за управление жизненным циклом подписок:
// src/app/services/app-message-registrator.service.ts
import { PostboyAbstractRegistrator } from '@artstesh/postboy';
@Injectable()
export class MessageRegistrator extends PostboyAbstractRegistrator {
constructor(postboy: AppPostboyService) {
super(postboy);
}
protected _up(): void { }
}
Инициализируем регистрацию в AppComponent :
// src/app.component.ts
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: true,
providers: [MessageRegistrator]
})
export class AppComponent implements OnDestroy {
constructor(private registrator: MessageRegistrator) {
registrator.up();
}
ngOnDestroy(): void {
this.registrator.down();
}
}
Подготовка завершена и приложение готово к работе с событиями в рамках подходов @artstesh/postboy
2. Использование библиотеки
Создадим событие(message) ButtonClickEvent
// src/app/messages/button-click.event.ts
export class ButtonClickEvent extends PostboyGenericMessage {
public static readonly ID = '5861b2ea-74eb-4744-9b04-69468a278c34';
constructor(public text: string){}
}
Зарегистрируем его в методе _up() регистратора:
// src/app/services/app-message-registrator.service.ts
import { PostboyAbstractRegistrator } from '@artstesh/postboy';
@Injectable()
export class MessageRegistrator extends PostboyAbstractRegistrator {
// ...
protected _up(): void {
this.recordSubject(ButtonClickEvent);
}
}
Вернемся к изначальному примеру и изменим поведение Отправителя и Получателя:
// Sender.component.ts
import { EventEmitter, Output } from '@angular/core';
@Component({
selector: 'app-sender',
template: `<button (click)="sendEvent()">Отправить</button>`,
})
export class SenderComponent {
constructor(private postboy: AppPostboyService) {}
sendEvent() {
this.postboy.fire(new ButtonClickEvent('Событие от отправителя!'));
}
}
// Receiver.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-receiver',
template: `<p>{{ message }}</p>`,,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ReceiverComponent implements OnInit {
message = '';
constructor(private postboy: AppPostboyService,
private detector: ChangeDetectorRef) {}
ngOnInit(): void {
this.postboy.sub(ButtonClickEvent).subscribe(ev => {
this.message = ev.text;
this.detector.detectChanges();
});
}
}
Теперь оба компонента взаимодействуют через AppPostboyService
, не зная о существовании друг друга. Это упрощает работу, особенно если количество таких взаимодействий растёт.
Что изменилось?
Мы полностью избавились от Input/Output связей. Нет необходимости настраивать события через родителя.
Реализация стала гибкой: теперь можно легко подключить новые компоненты-слушатели, просто подписав их на событие
ButtonClickEvent
.Такая архитектура легче тестируется: каждый компонент можно изолировать и протестировать логику отдельно.
Заключение
Если вы хотите упростить архитектуру своего Angular-приложения и сделать взаимодействие компонентов максимально прозрачным, полагаю, что @artstesh/postboy
станет весьма интересным вариантом. Она минималистична, проста в освоении и прекрасно интегрируется в существующие проекты.
Ваши вопросы, замечания и предложения очень важны! Делитесь своими идеями и обратной связью в комментариях или пишите мне в личных сообщениях. Больше информации можно найти на сайте проекта.
Спасибо за внимание! ?
gyach
а чем эта библиотека лучше signal`ов? Или они про разное?
artstesh Автор
Основа здесь - rxjs, а он, как писалось тут, 'никогда' не будет замещен signal'ами. Про относительность 'никогда' мы все все понимаем, но сейчас функционал разнится, особенно в части асинхронности. Та же логика executors из библиотеки, вероятно, в некоторой степени может быть замещена signal'ами, но либа больше про rxjs, чем про Angular, не у всех они есть) В общем, каждой задаче - свой инструмент, и это - ещё одна опция.
gyach
rxjs, как я понимаю, подходит для асинхронщины, когда мы ждем каких-то данных, и при поступлении пачек этих данных мы их обрабатываем. Сигналы, в свою очередь, - для синхронных операций (в нашем примере событие нажатия на кнопку).
Я спрашиваю потому, что ваши примеры упрощают жизнь для input/output, а с 19 версии документация официально input и output предлагает переводить на сигналы.
artstesh Автор
Так... По порядку:
signals во многом направлены на замещение механизма detectChanges в целях оптимизации рендеринга. В этой части никакой конкуренции с представленным решением нет.
В части передачи состояний все гораздо прозаичнее: я не вижу никакого ясного выигрыша от signals в задаче передачи данных между компонентами.
Есть у нас таблица элементов и input, при вводе таблица фильтруется в соответствии со значением input. Каким образом передавать состояние между элементами? Примитивный (и не самый лучший, согласен) пример с @Input - ужасная затея, скорее мы пойдем в Subject/ReplaySubject. Но как передать эту подписку между компонентами? Нужен сервис, содержащий объект подписки, оба компонента ссылаются на этот сервис и работают с подпиской. Signals, в этом смысле не предлагает ничего нового - мы так же должны будем создать сервис для передачи нашего signal. Со временем конструкторы компонентов распухают от ссылок на всевозможные сервисы, создавая сонм весьма неприятных связей. В моих проектах большинство компонентов зависят только от сервиса PostboyService, подписываясь на любое интересующее событие, не зная ничего об обработчиках/отправителях сообщений внутри системы.
P.S. За комментарий спасибо, учту на будущее, что стоит подсветить)