Прим. перев.: для понимания данной статьи необходимо обладать начальными знаниями Angular: что такое компоненты, как создать простейшее SPA приложение и т.д. Если Вы не знакомы с данной темой, то рекомендую для начала ознакомиться с примером создания SPA приложения из оф. документации.
Об NgModules можно прочитать здесь.
Один год назад я уже публиковал статью об 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