Архитектура приложения Angular. Используем NgModules

в 9:04, , рубрики: angular, javascript, TypeScript, архитектура приложений, Разработка веб-сайтов, разработка сайтов

Прим. перев.: для понимания данной статьи необходимо обладать начальными знаниями Angular: что такое компоненты, как создать простейшее SPA приложение и т.д. Если Вы не знакомы с данной темой, то рекомендую для начала ознакомиться с примером создания SPA приложения из оф. документации.

Об NgModules можно прочитать здесь.

image

Один год назад я уже публиковал статью об NgModules, где рассматриваются технические тонкости, когда импортировать модули, пространство имен и т.д. Рекомендуется для ознакомления (прим. перев.: статья по содержанию аналогична той, на которую я ссылаюсь вначале).

Недавно я принял вызов, который мне бросил Angular. До сих пор я использовал подход, предлагаемый официальной документацией Angular. Но дойдя до большого проекта стали проявляться недостатки.

Я начал детально изучать мануал по NgModules, который разросся аж до 12 страниц подробного описания с FAQ. Но после внимательного прочтения вопросов возникло больше, чем ответов. Например, где лучше реализовать сервис? Внятного ответа на этот вопрос получить не получилось. Более того, некоторые решения противоречат друг другу в контексте мануала.

После переваривания всего раздела про NgModules я решил реализовать свое решение по архитектуре Angular приложений, основанное на следующем:

  • структура: простая для малых приложений, масштабируемость для больших проектов;
  • юзабилити: возможность использования решений в других проектах;
  • оптимизация (в том числе с lazy load);
  • тестируемость.

Angular Modules

Что такое модули Angular?

На самом деле, главная цель модуля — группирование компонентов и/или сервисов, связанных друг с другом. И, в общем-то, больше ничего. Для примера, представим блок новостей на главной странице. Если грубо, то визуальная часть — это компонент, а механизм получения данных из базы данных — это сервис.

Для тех, кто знаком с Java, то модули Angular это пакеты (packages), а в C#/PHP — пространство имен.

Остается только один вопрос — как правильно группировать функционал приложения?

Типы модулей Angular

Их всего 3:

  • модули страниц;
  • модули сервисов;
  • модули компонентов для многократного использования.

Как только вы создали стартовое приложение через ng new projectname
то, как минимум, вы создали модуль страницы. В данном случае одной — главной.

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

Модули страниц

Модули страниц обладают маршрутизацией и предназначены для того, чтобы логически разделить области вашего приложения. Модули страниц загружаются один раз в главном модуле (который обычно называется AppModule) или через lazy load.

Для примера, на странице авторизации, выхода и регистрации нужен модуль AccountLogin; HeroesModule для страницы списка героев, страницы героя и т.д. (прим. перев.: здесь имеется ввиду учебный проект, который описывается в официальной документации).

Модули страниц могут содержать в себе:

  • /shared: сервисы и интерфейсы;
  • /pages: компоненты с маршрутами;
  • /components: компоненты для визуализации данных.

Общедоступные сервисы для страниц

Для отображения данных на странице, сначала нужно эти данные откуда-то взять. Для этого и нужны сервисы

@Injectable()
export class SomeService {

  constructor(protected http: HttpClient) {}

  getData() {
    return this.http.get<SomeData>('/path/to/api');
  }

}

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

Но для лучшей практики лучше спроектировать модуль так, чтобы конкретная страница требовала определенного типа данных, определенного сервиса. В таком случае нужно инкапсулировать данный сервис и ограничить доступ к нему внутри одного модуля, а не всего приложения.

Прим. перевод.

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

Давайте вернемся к модулю AccountManager, который был озвучен ранее в качестве примера. Сервис данного модуля, AccountService, должен быть "тонким" и отвечать, по необходимости, "да" или "нет", в зависимости от ролевой модели пользователя. Статус пользователя (онлайн или нет) не может быть реализован в данном сервисе, т.к. необходимость данного модуля может отсутствовать в некоторых частях приложения. Поэтому статус пользователя необходимо вынести в глобальный сервис, который будет доступен во всем приложении (см. ниже).

Модули-страницы: маршрутизация

Компонент страницы отвечает за представление информации из базы данных, которая извлекается сервисом.

Вы можете отображать данные непосредственно в компоненте, но Вы не обязаны этого делать. Вы можете передать данные в виде переменной в другой компонент

@Component({
  template: `<app-presentation *ngIf="data" [data]="data"></app-presentation>`
})
export class PageComponent {

  data: SomeData;

  constructor(protected someService: SomeService) {}

  ngOnInit() {
    this.someService.getData().subscribe((data) => {
      this.data = data;
    });
  }

}

Каждый компонент имеет свой маршрут.

Компоненты для визуализации данных

Компоненты для представления данных извлекают информацию при помощи декоратора @Input и отображают в своем шаблоне

@Component({
  selector: 'app-presentation',
  template: `<h1>{{data.title}}</h1>`
})
export class PresentationComponent {

  @Input() data: SomeData;

}

Это MVx?

Кто знаком с паттерном модель-контроллер-представление задастся вопросом — это оно самое? Если следовать теории, то нет. Однако, если Вам проще представить архитектуру Angular при помощи MVx, то:

services сравнимы с Models,
presentation components похожи на View,
page components будут Controllers Presenters ViewModels (выберете то, что вы используете).

Несмотря на то, что это не совсем MVx (или совсем не MVx), цели в данном подходе одинаковы — разделение ответственности в решении задач. Почему это важно? Вот почему:

  • "тонкие" компоненты (презентации) можно использовать в других проектах,
  • оптимизация стратегии обнаружения компонентов,
  • тестируемость "тонких" компонентов (если вы не разделяете логику приложения, то забудьте о тестировании, это будет сущий ад).

Суммируя

Пример модуля страницы

@NgModule({
  imports: [CommonModule, MatCardModule, PagesRoutingModule],
  declarations: [PageComponent, PresentationComponent],
  providers: [SomeService]
})
export class PagesModule {}

где сервис инкапсулирован в данном модуле.

Модули глобальных сервисов

Модули глобальных сервисов предоставляют доступ к своему сервису в любом месте Вашего приложения. Сервисы имеют тоже пространство имен, что и модули, загружаются только один раз и доступны везде, в т.ч. при реализации lazy load.

Вы определенно использовали хотя бы один такой сервис. Например: HttpModule. Но вскоре Вам понадобится свой сервис, похожий на HttpModule. Для примера — AuthModule, который хранит текущий статус пользователя и его токен, и необходим на протяжении всего приложения, всей сессии пользователя.

Юзабилити

Если Вы будете осторожный в проектировнии модуля для глобального сервиса, сделаете его без визуальной части, разобьете логику сервиса на отдельные модули и будете проектировать на уровне интерфейса, а не реализации конкретного приложения (т.е. не будете внедрять зависимости конкретного приложения), то такие модули могут быть использованы в других проектах.

Следует отметить, что если Вы хотите сделать модуль доступным в других проектах (т.е. из вне), необходимо создать для него точку входа, куда вы экспортируете NgModule, интерфейс и, возможно, токены для внедрения.

export { SomeService } from './some.service';
export { SomeModule } from './some.module';

Должен ли я делать CoreModule

Нет необходимости. Официальна документация предлагает реализовывать все глобальные сервисы в CoreModule. Вы, безусловно, можете сгруппировать их в /core/modules, однако уделите внимание разделению ответственности и не "сливайте" все в один CoreModule. Иначе Вы не сможете использовать реализованный функционал в других проектах.

Суммарно

Пример глобального модуля для сервиса

@NgModule({
  providers: [SomeService]
})
export class SomeModule {}

UI компоненты и как получать данные

UI компоненты (например виджеты) — "тонкие" и отвечают только за визуализацию полученных данных, как было рассмотрено выше в "модулях страниц". Компонент получает данные при помощи декоратора @Input (иногда из <ng-content>, а иногда и другие решения).

Component({
  selector: 'ui-carousel'
})
export class CarouselComponent {

  @Input() delay = 5000;

}

Вы не должны целиком полагаться на сервис. Почему? Потому что сервисы имеют свою специфику в зависимости от предложения. Например, может поменяться URL у API. Представление данных — дело компонентов внутри страниц модулей. UI компоненты получают данные, предоставленные кем-то, но не ими.

Открытые (public) и скрытые (private) компоненты

Для того, чтобы сделать компонент доступным (public) нужно экспортировать его в модуле. Однако, импортировать все не нужно. Вложенные компоненты должнымогут оставаться скрытыми (private), если в них нет необходимости в другом месте приложения.

Директивы и пайпы

Если говорить о модулях для директив и пайпов, то аналогично с UI компонентами. По необходимости экспортируем в модуле и используем там, где нам вздумается.

Скрытые (private) сервисы

Для работы с данными исключительно внутри UI компонента можно реализовать сервис только внутри компонента, а не NgModule и сделать его закрытым для всего, кроме его компонента. В таком случае это будет выглядеть так

@Component({
  selector: 'some-ui',
  providers: [LocalService]
})
export class SomeUiComponent {}

Общедоступные (public) сервисы

Представим ситуацию, когда Вы хотите открыть доступ к сервису, который реализовали в UI компоненте. Такое следует максимально избегать, но реализовать возможно.

Открываем доступ к сервису в NgModule и получаем проблему многократной загрузки модуля, а с ним и сервиса, т.к. в модуле мы реализуем компонент.

Для решения данной проблемы необходимо реализовать модуль таким образом

xport function SOME_SERVICE_FACTORY(parentService: SomeService) {
  return parentService || new SomeService();
}

@NgModule({
  providers: [{
    provide: SomeService,
    deps: [[new Optional(), new SkipSelf(), SomeService]],
    useFactory: SOME_SERVICE_FACTORY
  }]
})
export class UiModule {}

Кстати, так реализовано (по крайней мере было) в Angular CDK.

Юзабельность

Для использования UI компонентов в виде модулей, необходимо экспортировать компонентыпайпыдирективы и тд, открыть им доступ создав точку доступа

export { SomeUiComponent }  from './some-ui/some-ui.component';
export { UiModule } from './ui.module';

Нужно ли делать SharedModule?

Нужно ли сливать все весь пользовательский интерфейс (UI компоненты) в SharedModule Определенно нет. Хотя документация предлагает данное решение, но каждый модуль, реализованный в SharedModule будет реализован на уровне проекта, на не интерфейса.

Нет проблем в ипортировании зависимостей при создании проекта, особенно при помощи автоматизации этого процесса в VS Code (или других IDE).

Однако, куда лучшим тоном будет создать раздельные модули для каждой сущности пользовательского интерфейса и сложить их в папку /ui, например.

Суммарно

Пример UI модуля

@NgModule({
  imports: [CommonModule],
  declarations: [PublicComponent, PrivateComponent],
  exports: [PublicComponent]
})
export class UiModule {}

Что в итоге?

Если Вы будете проектировать Ваше приложение с учетом описанного выше, то:
Вы будете иметь хорошо структурированную архитектуру, будь то в малых или больших приложениях, с или без lazy load.
Вы можете упаковать глобальные модули или UI компоненты в библиотеки и использовать их в других проектах.
Вы будете тестировать приложения без агонии.

Пример структуры проекта

app/
|- app.module.ts
|- app-routing.module.ts
|- core/
   |- auth/
      |- auth.module.ts
      |- auth.service.ts
      |- index.ts
   |- othermoduleofglobalservice/
|- ui/
   |- carousel/
      |- carousel.module.ts
      |- index.ts
      |- carousel/
         |- carousel.component.ts
         |- carousel.component.css
    |- othermoduleofreusablecomponents/
|- heroes/
   |- heroes.module.ts
   |- heroes-routing.module.ts
   |- shared/
      |- heroes.service.ts
      |- hero.ts
   |- pages/
      |- heroes/
         |- heroes.component.ts
         |- heroes.component.css
      |- hero/
         |- hero.component.ts
         |- hero.component.css
   |- components/
      |- heroes-list/
         |- heroes-list.component.ts
         |- heroes-list.component.css
      |- hero-details/
         |- hero-details.component.ts
         |- hero-details.component.css
|- othermoduleofpages/

Если у Вас есть комментарии по данной архитектуре, то, пожалуйста, оставьте свои коментарии.

Telegram русскоязычного Angular сообщества.

Автор: chelovekkakvse

Источник

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


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