Оптимальный угловой: PubSub с NGRx

Концепции и практики, представленные в этой статье, описаны в предыдущей статье Практическая веб-разработка и архитектура. Разработка клиентского приложения на основе этих концепций будет объяснена в следующем изложении, которое было построено совместно с серверным приложением NodeJS и хранилищем данных Mongo. Обзор серверного приложения предлагается в статье Swagger, NodeJS, & TypeScript: TSOA. Код можно просмотреть в репозитории ws-ngx-login-demo GitHub . И интерфейсные, и внутренние приложения объединены в интегрированную среду разработки FullStack с использованием Docker, описанного в статье Docker - это мой {I.D.E}. Чтобы запустить это приложение в FullStack, клонируйте ws-dev-docker-example.

Для тех, кто не заинтересован в использовании Docker, но хочет клонировать, собирать и запускать приложение, у меня есть ветка create в репозитории GitHub, которая называется serverless , который может работать без использования Docker, серверного приложения NodeJS или Mongo.

Маршрут

В следующей статье демонстрируется использование последней версии Angular 4.1.0 (на момент написания этой статьи) и реализации NGRX (Redux) для демонстрации процесса регистрации и входа в систему с защищенным контентом.

  • Настройка для разработки. Кратко опишите инструменты и конфигурацию устройства для разработки.
  • Конфигурация каталога приложений: выделите содержание и подход к структуре каталогов приложений.
  • Управление и обработка данных: подробное объяснение того, как потоки данных и их обработка, содержится в разделе проблем. Будут использоваться средства предоставления представления профиля пользователя. чтобы обобщить процесс.

В разделе Управление и обработка данных приводится более подробная разбивка по нескольким областям, касающимся PubSubBroker и использования NGRx:

  • Создание представления профиля пользователя за 7 шагов: конкретные фрагменты кода, показывающие, как ProfileComponent на уровне представления взаимодействует с PubSubBroker на бизнес-уровне, чтобы облегчить взаимодействие с уровнем данных.
  • Деконструкция PubSubBroker & Brokerage: подробно описывает построение объектно-ориентированного подхода к созданию PubSubBroker и Brokerage для облегчения разделения уровня представления из уровня данных.
  • Обработка глобальных данных состояния с помощью NGRx: краткое описание того, как данные обрабатываются с помощью реализации Redux в NGRx.
  • Инкапсулированные службы: пример того, как службы бизнес-уровня вне PubSubBroker могут совместно использоваться компонентами уровня представления в виде инкапсулированного процессы. Это будет достигнуто путем определения того, как Реактивные формы Angular расширяются с помощью validator.js для настройки проверок с помощью компонента RegistrationComponent уровня представления.

Настройка разработки

Обработка исходного кода для этого проекта выполняется с помощью angular-seed Минко Гечева, который использует универсальный загрузчик модулей j spm’s SystemJS. В настоящее время последняя версия Angular X использует WebPack через инструмент angular-cli. WebPack полагается на накопление плагинов для обработки стратегий связывания и обслуживания задач, таких как компиляция препроцессора CSS, минификация и другие неприятности, связанные с созданием каркасов. SystemJS предназначен исключительно для агрегирования и упаковки модулей:

«Универсальный загрузчик динамических модулей - загружает модули ES6, AMD, CommonJS и глобальные скрипты в браузере и NodeJS»

Реализация мелочей выполнения задачи выполняется другими инструментами, такими как Gulp. Хотя WebPack может показаться автоматическим магическим подходом из одного окна, я обнаружил, что эта блаженная простота может превратиться в эпический крах, когда что-то пойдет не так. Я использовал оба инструмента, а пока придерживаюсь SystemJS.

angular-seed Минко Гечева представляет собой эффективный набор инструментов, который достаточно стабилен и хорошо документирован. Всякий раз, когда мне нужно внести изменения для настройки определенной задачи или включения сторонних библиотек, процесс по большей части был рутинным. В состав набора инструментов входит BrowserSync для запуска приложения в браузере с живой перезагрузкой.

Я загрузил Angular-seed Gechev в виде zip-файла и скопировал файлы в папку моего проекта, если это необходимо. Корневой каталог выглядит так:

Большая часть моих настроек для SASS и включение библиотек происходит в tools / config / seedconfig.ts и tools / config / project.config.ts . Однако я счел необходимым создать задачу для модульного тестирования, tools / tasks / seed / build.js.mockdata.ts,, которая инструктирует скаффолдинг компилировать и скопируйте данные тестирования в возможный каталог dist для модульного тестирования. Я также добавил некоторые атрибуты в tools / env / env-config.interface.ts, чтобы предоставить значения для хоста, идентификатора имени API и портов, которые в основном используются в моей экосистеме Docker.

Конфигурация каталога приложений

Angular-seed от Gechev - это ванильный стартовый набор для современного проекта Angular, и поэтому он не включает пару ресурсов, которые я буду использовать в этом проекте.

  • NGRx (ресурсы): обрабатывает резервное соединение для современных проектов Angular.
  • angular-material2: Материальный дизайн Google для современных Angular-проектов.
  • angular / flex-layout: библиотека директив для использования css flex.
  • @ types / validator: предоставление некоторых дополнительных атрибутов проверки для наших реактивных форм. (это пакет Definitively Typed, который представляет собой оболочку TypeScript вокруг библиотеки javascript, в данном случае validator.js)

После добавления этих ресурсов в качестве зависимостей в наш package.json и включения их в наш seed.config.ts мы готовы к работе.

src / client: содержит все каталоги и файлы, определяющие функциональное представление приложения.

src / e2e: содержит тестирование e2e (End To End) с транспортиром.

src / test-artifacts: содержит данные и вспомогательные файлы, используемые для модульного тестирования.

src / vendor: содержит сторонний код, который не вписывается в парадигму нашей структуры инструментов. В данном случае я включил прямую загрузку библиотеки Animate-sass с GitHub.

Каталог src / client / app / представляет собой процедурную базу кода приложения, и именно здесь мы создадим экземпляр нашей структуры Angular, загрузив корневой модуль.

src / client / app / app-stage: централизация файлов, необходимых для работы всего приложения.

src / client / app / shared-utils / app-env: содержит файл (ы), реализующий особенности среды, в которой создается приложение. Он работает вместе с файлами в папке tools / env.

src / client / assets: содержит все артефакты и изображения, используемые в приложении.

src / client / scss: содержит настроенный sass (например, переменные, миксины, абстрактные классы и т. д.), используемый для расширения стиля css, который по большей части структурирован в теме angular-material2. включен как зависимость в наше приложение.

src / client / css: содержит файл seed-main.css, реализующий сброс, и импортируется файлом main.scss, который действует в качестве подготовки для импорта файлов в src / client / scss. Основу такого подхода Гечев заложил в Wiki Работа с SASS.

Разделив различные задачи приложения, как описано в статье Практическая веб-разработка и архитектура, структура клиента сгруппирована соответствующим образом.

Просмотреть слой

src / client / app / view-layer: содержит файлы, реализующие представления приложений.

~ / view-layer / modules-by-route: каждый каталог, объединяющий файлы, необходимые для одного модуля, который служит основой для перспективного маршрута (просмотра URL). Модули из modules-by-route, которые не включены в app.stage.module, являются автономными модулями, которые загружаются по запросу с использованием метода «ленивой загрузки».

~ / view-layer / common-views: каждый каталог верхнего уровня представляет собой совокупность файлов для одного компонента представления, который может совместно использоваться в приложении.

Уровень данных

src / client / app / data-layer: реализация служб для удаленных (HTTP) транзакций данных, а также инфраструктуры для построения наших моделей данных в парадигме Redux. .

~ / data-layer / api-services: содержит оболочку (http.wrapper.services) для абстрагирования повторяющейся оркестрации реализации HTTP CRUD от каждой службы (например, user.services) API-запрос.

~ / data-layer / ngrx-data: реализует Redux с помощью NRGx.

  • редукторы: неизменяемые объекты данных, представляющие наш Redux Store.
  • Действия, охранники и эффекты содержат процедуры взаимодействия с Магазином.
  • ngrx.data.module.ts: объединяет эти ресурсы для экспорта в наш app.stage.module.

* Для более подробного и детального понимания того, как NGRx упрощает использование Redux в Angular, в учебнике блога ng-book « Введение в Redux с TypeScript и Angular 2 предлагается один из лучшие обзоры, которые я встречал. Несмотря на то, что это немного устарело, принципы все еще применяются. Чтобы быть в курсе того, как прогрессивные версии Angular применимы к NGRx, следуйте примеру приложения, которое Майк Райан поддерживает на GitHub.

Бизнес-уровень

src / client / app / business-layer: реализует процесс публикации и подписки для передачи запроса между уровнем представления. и уровень данных, что приводит к отсутствию прямого обмена между презентационными компонентами и оркестровкой NGRx Redux.

~ / бизнес-уровень / брокерство: реализует транзакции с уровнем данных путем регистрации в качестве потребителя в pubsub-broker .

~ / business-layer / pubsub-broker: реализует систему публикации и подписки для взаимодействия с уровнем данных.

~ / business-layer / models: содержит файлы, представляющие схемы данных, используемые как уровнем представления r, так и уровнем данных.

~ / business-layer / shared-types: содержит файлы, в которых перечислены различные типы действий, реализованные в data-layer / ngrx-data / actions в отношении Магазина и используемые заглушками. в брокерских / ngrx-stubs.

~ / business-layer / validator: содержит инкапсулированную реализацию службы для проверки вводимых пользователем данных с помощью форм, появляющихся в v уровне просмотра.

Управление и обработка данных

уровень данных: реализует всю HTTP-обработку для запроса и ответа на данные с сервера. Он также управляет состоянием данных с помощью шаблона Redux, наложенного NGRx, который формализует способ доступа к данным с помощью действий и селекторов.

бизнес-уровень: использует PubSubBroker вместе с брокером для ответа на запросы уровня представления о доступе к уровня данных, таким образом, удаляя любые ссылки на хранилище Redux уровня данных в представлении- слой. Заглушки-посредники ограничивают то, что каждый компонент уровня представления может потреблять из Магазина. Этот уровень Здесь также расположены инкапсулированные службы для обработки данных, не связанных с магазином.

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

Создание представления профиля пользователя за 7 шагов

В следующем сценарии показан процесс запроса данных профиля пользователя для отображения в ProfileComponen слоя представления. T. Он демонстрирует типичный сценарий, который уровень просмотра r использует с PubSubBroker на бизнес-уровне для наблюдения и запроса действий в отношении глобального состояния данные из уровня данных.

1 При создании экземпляра компонента уровня представления, такого как ProfileComponent с зависимостями данных, он отправляет именованный идентификатор Broker Stub , BROKER_PROFILE_STORE, с помощью функции dispatchBrokerSelector () в PubSubBroker, «BrokerDispatcherService.

export class ProfileComponent implements OnInit, OnDestroy {
 subscribeHandler:any;
 userProfile$:Observable<any>;
 brokerRef:any;
  //BrokerDispatcherService 
 constructor( private activatedRoute:ActivatedRoute,
              private bDS:BrokerDispatcherService) {

    var brokerResponse:BrokerResponse =         
this.bDS.dispatchBrokerSelector(BrokerList.BROKER_PROFILE_STORE);
   this.brokerRef = brokerResponse.brokerRequested;
                 
}
....

2 BrokerDispatcherService поддерживает установившиеся отношения с брокером-потребителем, например NGRxBrokerConsumer, через BrokerPublisher. В этом случае функция NGRxBrokerConsumer ReceivedBrokerSelectorRequest () использует именованный идентификатор для ссылки на заглушку брокера, BrokerProfileStore.

export class NGRxBrokerConsumer implements IConsumer{
  constructor( private brokerDialogStore: BrokerDialogStore,
               private brokerLoginStore: BrokerLoginStore,
               private brokerNavStore: BrokerNavStore,
               private brkrRgstrtnStore: BrokerRegistrationStore,
               private brokerProfileStore: BrokerProfileStore   ){ }

            ....
 
 public ReceivedBrokerSelectorRequest(brokerType:string):     
        BrokerResponse{
        var brokerResponse = new BrokerResponse();
        switch(brokerType){
             ....
            case this.brokerProfileStore.brokerLabel:
                  brokerResponse.brokerRequested =
                    this.brokerProfileStore.getComponentSupplies();
            break;
        }
        return brokerResponse;
    }

}

3 Заглушка BrokerProfileStore отвечает на вызов функции getComponentSupplies (), возвращая объект, содержащий свойства для 'storeObs' и ' storeDsp '. storeObs содержит Observables, производные от Магазин NGRx S выборщиков. storeDsp содержит набор BrokerActions для отправки действий в отношении Store .

export class BrokerProfileStore {
    brokerLabel:string = BrokerList.BROKER_PROFILE_STORE;
    constructor( private store: Store<fromRoot.State>,
                 private brkrActnBuilder:BrokerActionBuilder ) { } 
    getComponentSupplies():any{ return  
      Object.assign({
        brokerLabel: this.brokerLabel,
        storeObs:{ userProfile$: this.store.
                             select(fromRoot.getSelectedProfile)  
                  },
        storeDsp:{ GET_USER_PROFILE_ATTEMPT: this.brkrActnBuilder.
               create( ProfileActionTypes.GET_USER_PROFILE_ATTEMPT,
                       this.brokerLabel, null)
                  }
       });
    }


}

4 После получения BrokerResponse компонент уровня представления извлекает подписки на Observables, содержащиеся в ' storeObs ', которое обеспечивает постоянную осведомленность о состоянии атрибута userProfile $ в Магазин.

//Profile Component continued..
ngOnInit() {
         
  this.userProfile$ = this.brokerRef.storeObs.userProfile$;
    .....
}

5 Действия против Store, которые может выполнять ProfileComponent, содержатся в файле «storeDsp». Запрос отправляется с помощью метода dispatchBrokerAction () PubSubBroker's BrokerDispatcherService. В этом случае имя пользователя, предоставленный в качестве атрибута параметра в URL-адресе, отправляется как действие.

//Profile Component continued..
ngOnInit() {
         ...
 this.subscribeHandler = this.activatedRoute.params.
      subscribe(params => {
        var note = this.brokerRef.storeDsp.GET_USER_PROFILE_ATTEMPT;
        note.payLoad =  params['username'];
        this.bDS.dispatchBrokerAction(note);
      });
}

6 И снова NGRxBrokerConsumer обработает запрос. Однако, поскольку это требование для действия, запрос будет получен методом ReceiveBrokerAction, который затем передаст запрос в Заглушка BrokerProfileStore с помощью функции dispatchAction ().

export class NGRxBrokerConsumer implements IConsumer{

            ....
 
public ReceiveBrokerAction( brokerAction:BrokerAction){
    switch(brokerAction.brokerType){
           ...
        case this.brokerProfileStore.brokerLabel:
              this.brokerProfileStore.dispatchAction(brokerAction);
        break;
    }
}

7 Брокер будет использовать свою установленную ссылку на Магазин для отправки нового Действия «profileActions.GetUserProfileAttempt» против состояния Магазина.

@Injectable()
export class BrokerProfileStore {
    brokerLabel:string = BrokerList.BROKER_PROFILE_STORE;
    constructor( private store: Store<fromRoot.State>,
                 private brkrActnBuilder:BrokerActionBuilder ) { }
dispatchAction(brokerAction:BrokerAction):void {
  switch(brokerAction.actionType){
    case ProfileActionTypes.GET_USER_PROFILE_ATTEMPT:
      this.store.dispatch(new 
                    profileActions.GetUserProfileAttempt(        
                            brokerAction.payLoad));  
            break;
   }
}

Точная реализация того, что происходит после вызова функции ProfileAction GetUserProfileAttempt (), будет подробно описано позже в разделе Обработка глобальных данных состояния с помощью NGRx.

Деконструкция PubSubBroker и брокера

На бизнес-уровне модули PubSubBroker и Brokerage обрабатывают обмен данными от компонентов на уровне представления. к основным объектам данных на уровне данных. Обработка связи между двумя отдельными уровнями с помощью PubSub Broker позволяет полностью разделить механизмы обработки состояния нашего объекта данных. Если наш уровень данных должен измениться на другой шаблон для поддержания состояния, возможно, больше не будет использовать хранилище Redux, ни один из визуальных компонентов не понадобится для изменения, пока новый уровень данных предоставляет наблюдаемые для подписки на состояние объектов данных, и реализовать механизм обновления состояния объекта данных с некоторым подобием объекта действия .

Модуль PubSubBroker (README.md) реализован с помощью объектно-ориентированных средств TypeScript.

  1. Абстрактный класс AbstractBrokerTrader расширяет ISupplier и имеет атрибут массива типа IConsumer.
  2. Как абстрактный класс, AbstractBrokerTrader, нельзя создать экземпляр, но он должен быть расширен другим классом, который выполняется BrokerPublisher.
  3. Поскольку BrokerPublisher расширяет AbstractBrokerTrader, он является не только ISпоставщиком, но также осведомлен о Тип атрибута массива IConsumer .
  4. В модуле Брокерские услуги есть каталог «реестры», содержащий такие службы, как NGRxBrokerRegistrationService. Эти службы реализуют регистрацию между брокерскими IConsumers, такими как NGRxBrokerConsumer, определение класса которого реализует IConsumer и BrokerPublisher.
  5. Эта регистрация выполняется, когда NGRxBrokerRegistrationService вызывает функцию BrokerPublisher RegisterBrokerConsumer (). Функция BrokerPublisher помещает экземпляры типов IConsumer, например NGRxBrokerConsumer, в свой массив IConsumer.
  6. BrokerDispatchService, внедренный в компоненты , находящиеся на уровне представления, содержит ссылку на BrokerPublisher и, таким образом, может вызывать функции BrokerPublisher, которые могут ссылаться на любую из функций объектов массива IConsumer, например ReceiveBrokerAction ().

Единственные существующие брокеры, которые существуют в настоящее время, используются для предоставления состояния и выполнения действий в отношении аппарата управления состоянием NGRx в уровень данных, другие брокеры могут быть созданы таким же образом, если требуется альтернативный шаблон или система управления.

Каталог модуля брокера, ngrx-stubs, содержит различные заглушки брокера, используемые для сбора селекторов и Действия, необходимые для определенных компонентов на уровне представления. Каждая заглушка имеет прямую ссылку на Store NGRx на уровне данных и создается на бизнес-уровне от e NGRxBrokerConsumer.

Обработка глобальных данных о состоянии с помощью NGRx

(каждый шаг обозначен цифрой в желтом кружке)

0 Компоненты подписываются на изменения в состоянии объекта Store (часто называемого интеллектуальным компонентом или контейнерами) посредством использования Селекторы, чтобы установить наблюдаемый, который на диаграмме выше обозначен как Цель.

// While this is often place in the ngOnInit() or constructor() of // Angular components, this call resides in 
// brokerage/ngrx-stubs/broker.profile.store 
this.store.select(fromRoot.getSelectedProfile)

1 Инициирование цикла данных начинается, когда отправляется новое действие.

this.store.dispatch(
     new profileActions.CheckUserProfileNameAttempt('wilSonic')
           );

В приведенном выше операторе 'this.store.dispatch' созданный новый объект настраивается в файле объявления действия, профиль. .action.ts.

export class CheckUserProfileNameAttempt implements Action {
 public readonly type = ProfileActionTypes.CHECK_USER_PROFILE_NAME_ATTEMPT;
  constructor(public payload:string) {  }
}

2 Отправляя действие в Магазин, оно сначала проходит проверку Редуктора. для case, чтобы взаимодействовать с типом Action. Если Состояние Редуктора изменяется в зависимости от условий, заданных в случае, все Наблюдаемые активно подписываются путем из Селекторов, будет уведомлен об изменении. (… такой Observable был установлен на нулевом шаге)

Конкретное действие «CHECK_USER_PROFILE_NAME_ATTEMPT», которое инициировало наш курс действий, не имеет отношения ни к одному из редукторов. Тем не менее, в конечном итоге его подхватит Эффект, описанный на третьем шаге .

export  interface State {
  ids: string[];
  entities: { [id: string]: UserModel };
  selectedProfileId: string | null;
  validUserName:string;
}

export const initialState: State = {
  ids: [],
  entities: {},
  selectedProfileId: null,
  validUserName:null
};
export function reducer(state = initialState, action:   
                               profileActions.Actions): State {
switch (action.type) {
        .....

      case ProfileActionTypes.SET_SELECTED_PROFILE_ID:{
          if (state.ids.indexOf(action.payload) > -1) {
             return Object.assign({}, 
                              state, 
                             { selectedProfileId:action.payload });
          }else {
             return state;
          }
      }

     default: {
         return state;
     }
}

3Как и редукторы, эффекты могут регистрировать запрос на взаимодействие с определенными типами действий. Таким образом, наше первоначально отправленное действие продолжает свое выполнение, поскольку ProfileEffects зарегистрировал запрос на взаимодействие с типом действия CHECK_USER_PROFILE_NAME_ATTEMPT. .

export class ProfileEffects {
@Effect() getUserByName$ = this.actions$
 .ofType(profileActions.ProfileTypes.CHECK_USER_PROFILE_NAME_ATTEMPT) .map(action => action.payload)
 .switchMap(payload => 
    this.userServices.getUserByName( 
      payload,
      errorActions.ErrorTypes.REPORT_ERROR,
      profileActions.ProfileTypes.CHECK_USER_PROFILE_NAME_FAILURE,
      profileActions.ProfileTypes.CHECK_USER_PROFILE_NAME_SUCCESS)
  );

 constructor( private store:Store<fromRoot.State>,
              private userServices: UserServices,
              private actions$: Actions
             ) {  }
}

4 Хотя упрощенная обработка бизнес-правил может быть применена путем манипулирования Observable с помощью реактивной библиотеки RxJs, часто бывает так, что отображение Observable в отдельный сервис для обработки - лучший подход. Это особенно верно, когда требуются асинхронные службы. Мы используем такую ​​службу в вышеупомянутом Эффекте, реализуя UserService (this.userServices.getUserByName (…)) для запроса информации API.

export class UserServices {

  constructor(private httpWrapperService: HttpWrapperService) { }

     getUserByName( payload:string,
                    ErrorActionType:string, 
                    SpecificErrorType:string, 
                    SuccessType:string) {
         let getParams: HttpParams = {
            auth: false,
            errorActionType: ErrorActionType,
            specificErrorType:SpecificErrorType,
            payload: payload,
            responseObject: 'user',
            successActionType:SuccessType,
            uri: 'Users/username/'+payload
         };
         return this.httpWrapperService.get(getParams);
       }
}

В то время как методы UserService запрашивают транзакции HTTP, фактический AJAX определяется в оболочке HttpWrapperService (). Результаты успешного HTTP-запроса сопоставляются с текущим Observable, обрабатываемым ProfileEffect. Другими словами, поток Observable все еще активен, когда происходит успешный результат HTTP-запроса. Когда возникает ошибка, текущий Observable больше не работает. Подобно разрешению Promise путем создания Promise.reject, новый Observable должен быть определен, как в случае с оператором catch (), где используется Observable.of (), сопоставляя результирующее действие 'error action .type '.

export class HttpWrapperService {

  constructor(private http: Http) { }
...
public get(params: HttpParams) {
    let {apiUrl, options} = 
           this.configRequest(params.uri, params.auth);
     return this.http.get(apiUrl, options)
           .map(res => ({
                type: params.successActionType,
                payload: res.json()[params.responseObject]
             }))
           .catch(res => Observable.of({
              type: params.errorActionType,
               payload: {
               action_type: params.specificErrorType,
               message: res.json()
             }
      }));
  }
...

5 Независимо от того, какие процедуры выполняются во время выполнения задачи Эффекты, результатом процесса будет Наблюдаемый объект Действие, который снова пройдет цикл Redux.

.map(res => ({
                type: params.successActionType,
                payload: res.json()[params.responseObject]
             }))

Приведенный выше фрагмент кода демонстрирует сопоставление результата с типом действия профиляCHECK_USER_PROFILE_NAME_SUCCESS’, который создает объект Action ниже:

export class CheckUserProfileNameSuccess implements Action {
 public readonly type =  
               ProfileActionTypes.CHECK_USER_PROFILE_NAME_SUCCESS;
  constructor(public payload:UserModel) {  }
}

В то время как неудачная попытка приведет к появлению типа действия при ошибке: «REPORT_ERROR», который создаст следующий объект действия:

export class ReportError implements Action {
  public readonly type = ErrorActionTypes.REPORT_ERROR;
  constructor(public payload: ErrorModel) {  }
}

Инкапсулированные услуги:

Наконец, есть экземпляр Реактивных форм Angular, который используется в приложении, когда пользователь вводит данные в регистрационную форму. Процесс проверки вводимых пользователем данных был дополнен набором схем проверки из validator.js. Этот микро процесс не использует NGRx Redux каким-либо существенным образом, кроме проверки, используется ли уже имя пользователя.

Короче говоря, модуль Регистрация импортирует Компоненты управляющих сообщений и Службы проверки для отображения сообщений об ошибках под полями ввода. После ввода текста и нажатия кнопки табуляции или просто перемещения фокуса в следующее поле ввода данные из предыдущего поля ввода обрабатываются службами проверки. Если в процессе проверки обнаружена ошибка,

*ngIf="errorMessage !== null"

Условие * ngIf Angular в шаблоне компонента управляющего сообщения становится истинным и отображает сообщение.

Надеюсь, это приложение демонстрирует, как современный проект Angular может быть структурирован на основе «разделения задач». Хотя архитектура может показаться немного сложной для выполняемых черных задач (регистрация и вход в систему), цель этих усилий - предоставить пример структуры для больших приложений. Независимо от того, созданы и поддерживаются одним разработчиком или несколькими разработчиками, концепции, изложенные в этой статье, могут служить примером конструктивного подхода к разработке основы для максимальной масштабируемости.