- PVSM.RU - https://www.pvsm.ru -
Прим. перев.: для понимания данной статьи необходимо обладать начальными знаниями Angular: что такое компоненты, как создать простейшее SPA приложение и т.д. Если Вы не знакомы с данной темой, то рекомендую для начала ознакомиться с примером создания SPA приложения [1] из оф. документации.
Об NgModules можно прочитать здесь [2].
Один год назад я уже публиковал статью об NgModules [3], где рассматриваются технические тонкости, когда импортировать модули, пространство имен и т.д. Рекомендуется для ознакомления (прим. перев.: статья по содержанию аналогична той, на которую я ссылаюсь вначале).
Недавно я принял вызов, который мне бросил Angular. До сих пор я использовал подход, предлагаемый официальной документацией Angular. Но дойдя до большого проекта стали проявляться недостатки.
Я начал детально изучать мануал по NgModules [4], который разросся аж до 12 страниц подробного описания с FAQ. Но после внимательного прочтения вопросов возникло больше, чем ответов. Например, где лучше реализовать сервис? Внятного ответа на этот вопрос получить не получилось. Более того, некоторые решения противоречат друг другу в контексте мануала.
После переваривания всего раздела про NgModules
я решил реализовать свое решение по архитектуре Angular приложений, основанное на следующем:
На самом деле, главная цель модуля — группирование компонентов и/или сервисов, связанных друг с другом. И, в общем-то, больше ничего. Для примера, представим блок новостей на главной странице. Если грубо, то визуальная часть — это компонент, а механизм получения данных из базы данных — это сервис.
Для тех, кто знаком с Java, то модули Angular это пакеты (packages), а в C#/PHP — пространство имен.
Остается только один вопрос — как правильно группировать функционал приложения?
Их всего 3:
Как только вы создали стартовое приложение через ng new projectname
то, как минимум, вы создали модуль страницы. В данном случае одной — главной.
По мере того, как Ваше приложение будет расти, вы будете создавать новые модули для страниц, сервисов, компонентов и группировать их между собой. Если, конечно, вы хотите получить обслуживаемое и масштабируемое приложение, а не слить весь функционал в одном файле.
Модули страниц обладают маршрутизацией и предназначены для того, чтобы логически разделить области вашего приложения. Модули страниц загружаются один раз в главном модуле (который обычно называется AppModule
) или через lazy load.
Для примера, на странице авторизации, выхода и регистрации нужен модуль AccountLogin
; HeroesModule
для страницы списка героев, страницы героя и т.д. (прим. перев.: здесь имеется ввиду учебный проект [1], который описывается в официальной документации).
Модули страниц могут содержать в себе:
Для отображения данных на странице, сначала нужно эти данные откуда-то взять. Для этого и нужны сервисы
@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 [5] и отображают в своем шаблоне
@Component({
selector: 'app-presentation',
template: `<h1>{{data.title}}</h1>`
})
export class PresentationComponent {
@Input() data: SomeData;
}
Кто знаком с паттерном модель-контроллер-представление задастся вопросом — это оно самое? Если следовать теории, то нет. Однако, если Вам проще представить архитектуру 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 [7]. Но вскоре Вам понадобится свой сервис, похожий на HttpModule
. Для примера — AuthModule
, который хранит текущий статус пользователя и его токен, и необходим на протяжении всего приложения, всей сессии пользователя.
Если Вы будете осторожный в проектировнии модуля для глобального сервиса, сделаете его без визуальной части, разобьете логику сервиса на отдельные модули и будете проектировать на уровне интерфейса, а не реализации конкретного приложения (т.е. не будете внедрять зависимости конкретного приложения), то такие модули могут быть использованы в других проектах.
Следует отметить, что если Вы хотите сделать модуль доступным в других проектах (т.е. из вне), необходимо создать для него точку входа, куда вы экспортируете NgModule, интерфейс и, возможно, токены для внедрения.
export { SomeService } from './some.service';
export { SomeModule } from './some.module';
Нет необходимости. Официальна документация предлагает реализовывать все глобальные сервисы в CoreModule. Вы, безусловно, можете сгруппировать их в /core/modules, однако уделите внимание разделению ответственности и не "сливайте" все в один CoreModule
. Иначе Вы не сможете использовать реализованный функционал в других проектах.
Пример глобального модуля для сервиса
@NgModule({
providers: [SomeService]
})
export class SomeModule {}
UI компоненты (например виджеты) — "тонкие" и отвечают только за визуализацию полученных данных, как было рассмотрено выше в "модулях страниц". Компонент получает данные при помощи декоратора @Input [5] (иногда из <ng-content>, а иногда и другие решения).
Component({
selector: 'ui-carousel'
})
export class CarouselComponent {
@Input() delay = 5000;
}
Вы не должны целиком полагаться на сервис. Почему? Потому что сервисы имеют свою специфику в зависимости от предложения. Например, может поменяться URL у API. Представление данных — дело компонентов внутри страниц модулей. UI компоненты получают данные, предоставленные кем-то, но не ими.
Для того, чтобы сделать компонент доступным (public) нужно экспортировать его в модуле. Однако, импортировать все не нужно. Вложенные компоненты должнымогут оставаться скрытыми (private), если в них нет необходимости в другом месте приложения.
Если говорить о модулях для директив и пайпов, то аналогично с UI компонентами. По необходимости экспортируем в модуле и используем там, где нам вздумается.
Для работы с данными исключительно внутри UI компонента можно реализовать сервис только внутри компонента, а не NgModule
и сделать его закрытым для всего, кроме его компонента. В таком случае это будет выглядеть так
@Component({
selector: 'some-ui',
providers: [LocalService]
})
export class SomeUiComponent {}
Представим ситуацию, когда Вы хотите открыть доступ к сервису, который реализовали в 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';
Нужно ли сливать все весь пользовательский интерфейс (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 [8] русскоязычного Angular сообщества.
Автор: chelovekkakvse
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/275887
Ссылки в тексте:
[1] примером создания SPA приложения: https://angular.io/tutorial
[2] здесь: https://habrahabr.ru/post/351504/
[3] статью об NgModules: https://medium.com/@cyrilletuzi/understanding-angular-modules-ngmodule-and-their-scopes-81e4ed6f7407
[4] мануал по NgModules: https://angular.io/guide/ngmodules
[5] декоратора @Input: https://angular.io/guide/component-interaction#pass-data-from-parent-to-child-with-input-binding
[6] стратегии обнаружения компонентов: https://angular.io/api/core/ChangeDetectionStrategy
[7] HttpModule: https://angular.io/guide/http
[8] Telegram : http://t.me/angular_ru
[9] Источник: https://habrahabr.ru/post/351678/?utm_campaign=351678
Нажмите здесь для печати.