Как упростить взаимодействие компонентов в Angular-приложении с помощью @artstesh-postboy

в 9:15, , рубрики: angular, event-driven, Events, rxjs, TypeScript

Описание проблемы

Если вы разрабатывали приложения на 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 создана, чтобы:

  1. Снизить связанность кода — больше никаких прямых связей компонентов через Input/Output или событийные Subjects.

  2. Упростить архитектуру — теперь каждый компонент может "общаться" с другими через глобальный механизм событий, не зная детали их реализации.

  3. Минимизировать зависимости — лёгкий инструмент, который не требует громоздких библиотек или решений.

@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, не зная о существовании друг друга. Это упрощает работу, особенно если количество таких взаимодействий растёт.

Что изменилось?

  1. Мы полностью избавились от Input/Output связей. Нет необходимости настраивать события через родителя.

  2. Реализация стала гибкой: теперь можно легко подключить новые компоненты-слушатели, просто подписав их на событие ButtonClickEvent.

  3. Такая архитектура легче тестируется: каждый компонент можно изолировать и протестировать логику отдельно.

Заключение

Если вы хотите упростить архитектуру своего Angular-приложения и сделать взаимодействие компонентов максимально прозрачным, полагаю, что @artstesh/postboy станет весьма интересным вариантом. Она минималистична, проста в освоении и прекрасно интегрируется в существующие проекты.

Ваши вопросы, замечания и предложения очень важны! Делитесь своими идеями и обратной связью в комментариях или пишите мне в личных сообщениях. Больше информации можно найти на сайте проекта.

Спасибо за внимание! 😊

Автор: artstesh

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js