Хабрахабр

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

Прим. перев.: для понимания данной статьи необходимо обладать начальными знаниями 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 {}

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

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

Модули глобальных сервисов предоставляют доступ к своему сервису в любом месте Вашего приложения. Так как такие сервисы имеют глобальную область видимости, эти модули загружаются только один раз в корневой модуль (AppModule) и доступны везде, в т.ч. при реализации 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 сообщества.

Показать больше

Похожие публикации

Кнопка «Наверх»