Асинхронное управление событиями в Angular-приложениях — одна из основных задач при разработке, особенно если приложение активно взаимодействует с динамическими данными или имеет сложную архитектуру. Зачастую для решения этой задачи разработчики используют подписки и/или промежуточные сервисы. Несмотря на популярность этих подходов, они могут приводить к избыточным зависимостям, высокому уровню связности и сложному коду.

Библиотека @artstesh/postboy предоставляет альтернативный взгляд на эту проблему: она позволяет избавиться промежуточных сервисов и использовать асинхронные события простым и лаконичным способом.

Как отправлять и обрабатывать асинхронные события

*В данном опусе пропущены все объяснения того, что такое @artstesh/postboy и как его устанавливать, за этой информацией можете заглянуть сюда или на сайт проекта.

Отправка событий

Для публикации событий в @artstesh/postboy используется удобный интерфейс, позволяющий публиковать события с параметрами или без них. Пример отправки события:

// Отправка события без параметров
postboy.fire(new SimpleEvent());

// Отправка события с параметрами
postboy.fire(new EventWithParams('Hello, world!'));

В этом примере библиотека полностью берет на себя задачу передачи сообщений. Благодаря этому вам не нужно вручную связывать компоненты через сервисы или использовать Input/Output. Также стоит отметить, что и complete подписок библиотека так же возьмет на себя (в конце жизненного цикла модуля в котором обитает компонент).


Обработка событий

Для обработки событий достаточно подписаться через саму библиотеку. Это не стандартная подписка RXJS/Observable, а способ привязки, при котором (с некоторыми оговорками) стирается необходимость в явных отписках.

Пример обработки событий:

// Подписка на событие без параметров
postboy.sub(SimpleEvent).subscribe(() => {
  console.log('Событие simpleEvent выполнено!');
});

// Подписка на событие с параметрами
postboy.sub(EventWithParams).subscribe(message => {
  console.log('Получено сообщение:', message);
});

Теперь при вызове postboy.fire(new SimpleEvent()) метод обработки автоматически выполнится. Если событие передавало данные, они будут доступны в обработчике.


Чем это лучше традиционных подходов

Преимущества подхода @artstesh/postboy:

Сокращение кода: Больше никакого создания промежуточных сервисов. Одного экземпляра Postboy достаточно для сообщения между всеми частями приложения (либо отдельными его модулями, если такое разделение будет ��еобходимо, тогда мы разделяем зоны событий, выделяя на каждую зону отдельный регистратор).

Повышение читаемости кода: Нет громоздких цепочек подписок и управлений состояниями. Роль каждого компонента становится явно определённой.

Ослабление проблемы утечек памяти: При использовании библиотеки не нужно явно отписываться от событий — как это необходимо, например, в случае с RXJS, система вызовет complete() на всех подписках завершаемого модуля.

Гибкость управления: Возможность легко изменять поведение приложения при работе с событиями, добавлять новые сценарии обработки событий, удалять или перенаправлять события без необходимости вносить изменения в саму бизнес-логику компонентов или жестко связывать их друг с другом. Это упрощает развитие приложения и его поддержку в долгосрочной перспективе.


Разница подходов

Давайте рассмотрим пример синтетического сценария: у нас есть кнопка, которая инициирует запрос и показывает результат пользователю в компоненте.

Без @artstesh/postboy

В стандартной архитектуре Angular это могло бы выглядеть примерно так:

//middle.service.ts

import {Injectable} from "@angular/core";
import {ReplaySubject} from "rxjs";

@Injectable({providedIn: 'root'})
public class MiddleService {
  message$ = new ReplaySubject<string>()
}
// button.component.ts
import { Component } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app-button',
  template: '<button (click)="loadData()">Загрузить данные</button>',
})
export class ButtonComponent {
  constructor(private dataService: DataService, private middle: MiddleService) {}

  loadData() {
    this.dataService.loadData().subscribe((data) => {
      this.middle.message$.next(data.result);
    });
  }
}
// display.component.ts
import {Component, Input, OnInit} from '@angular/core';

@Component({
  selector: 'app-display',
  template: '{{ data }}',
})
export class DisplayComponent implements OnInit {
  data: string = '';

  constructor(private middle: MiddleService) {
  }

  ngOnInit() {
    this.middle.message$.subscribe(d => this.data = d);
  }
}

С @artstesh/postboy

Теперь перепишем этот пример с использованием нашей библиотеки:

// button.component.ts
import { Component } from '@angular/core';
import { AppPostboyService } from './app-postboy.service';

@Component({
  selector: 'app-button',
  template: '<button (click)="loadData()">Загрузить данные</button>',
})
export class ButtonComponent {

  constructor(private postboy: AppPostboyService) {}

  loadData() {
    this.postboy.fireCallback(new LoadDataCommand(),response => {
      // Логика после загрузки данных
      const data = { result: 'Данные успешно загружены' };
      this.postboy.fire(new DataLoadedEvent(data));
    });
  }
}
// display.component.ts
import {Component, OnInit} from '@angular/core';
import {Postboy} from '@artstesh/postboy';

@Component({
  selector: 'app-display',
  template: '{{ data }}',
})
export class DisplayComponent implements OnInit {
  data: string = '';

  constructor(private postboy: AppPostboyService) {}

  ngOnInit() {
    this.postboy.sub(DataLoadedEvent).subscribe(data => {
      this.data = data.result;
    });
  }pp

Для пары компонентов разница, возможно, не выглядит действительно впечатляющей, но с увеличением масштаба разница становится все более заметной - оригинальный код обрастает все большим количеством подписок и новых сервисов, а управляемый postboy все так же зависит только от AppPostboyService


Заключение

Асинхронное управление событиями с библиотекой @artstesh/postboy позволяет сделать код в Angular-приложениях более понятным и простым в сопровождении. Благодаря тому, что вам больше не нужно создавать промежуточные сервисы, объединять компоненты через Input/Output, работа с событиями становится гораздо проще.

Не забудьте ознакомиться с другим функционалом библиотеки, а еще я буду рад услышать предложения по улучшению и дополнению функционала.