Статьи

Использование угловых NgModules для многоразового кода и многое другое

NgModules — это основная концепция Angular, которая является частью каждого приложения и помогает связать некоторые важные детали для компилятора и среды выполнения приложения. Они особенно полезны для организации кода в функции, ленивых загрузочных маршрутов и создания повторно используемых библиотек.

В этом руководстве мы рассмотрим основные способы использования NgModules с некоторыми примерами, чтобы показать вам, как использовать их в ваших проектах Angular! Это руководство предполагает, что у вас есть практические знания по Angular.

Модули JavaScript не являются модулями Ng

Давайте сначала расскажем о том, что такое модули JavaScript (иногда называемые модулями ES6). Это языковая конструкция, которая упрощает организацию вашего кода.

По своей сути, модули Javascript — это файлы JavaScript, которые содержат ключевые слова import или export и которые делают объекты, определенные внутри этого файла, частными, если вы не экспортируете их. Я рекомендую вам ознакомиться с приведенной выше ссылкой для более глубокого понимания, но, по сути, это способ упорядочить ваш код и легко обмениваться им, не полагаясь на ужасную глобальную область.

Когда вы создаете приложение Angular с TypeScript, каждый раз, когда вы используете import или export в своем источнике, оно рассматривается как модуль JavaScript. TypeScript может обработать загрузку модуля для вас.

Примечание: для ясности в этой статье я всегда буду ссылаться на модули JavaScript и NgModules по их полным именам.

Основной NgModule, AppModule

Давайте начнем с рассмотрения базового NgModule, который существует в каждом приложении Angular, AppModule (который генерируется по умолчанию в любом новом приложении Angular). Это выглядит примерно так:

 import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } 

Angular использует декораторы для определения метаданных, о которых он должен знать во время компиляции. Чтобы определить NgModue, вы просто добавляете декоратор @NgModule() над классом. Класс не всегда может быть пустым, но часто это так. Однако вам нужно определить объект с некоторыми свойствами, чтобы NgModule мог что-либо делать.

Когда приложение загружается, ему нужно дать NgModule для создания экземпляра. Если вы загляните в основной файл вашего приложения (также обычно называемый main.ts ), вы увидите platformBrowserDynamic().bootstrapModule(AppModule) , который показывает, как приложение регистрирует и инициирует AppModule (который может называться как угодно, но почти всегда называют это).

Свойства NgModule

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

providers

providers — это массив, который содержит список любых поставщиков (инъекционных услуг), доступных для этого NgModule. У провайдеров есть область действия, и если они перечислены в лениво загруженном NgModule, они не доступны вне этого NgModule.

declarations

Массив declarations должен содержать список любых директив, компонентов или каналов, которые определяет этот NgModule. Это позволяет компилятору найти эти элементы и убедиться, что они связаны правильно. Если это корневой NgModule, то объявления доступны для всех NgModule. В противном случае они видны только одному и тому же модулю NgModule.

imports

Если ваш NgModule зависит от любых других объектов из другого NgModule, вам придется добавить его в массив imports . Это гарантирует, что компилятор и система внедрения зависимостей знают об импортируемых элементах.

exports

Используя массив export, вы можете определить, какие директивы, компоненты и каналы доступны для любого NgModule, который импортирует этот NgModule. Например, в библиотеке пользовательского интерфейса вы должны экспортировать все компоненты, которые составляют библиотеку.

entryComponents

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

bootstrap

Вы можете определить любое количество компонентов для начальной загрузки при первой загрузке приложения. Обычно вам нужно только загрузить основной корневой компонент (обычно называемый AppComponent ), но если у вас есть более одного корневого компонента, каждый из них будет объявлен здесь. Добавляя компонент в массив bootstrap , он также добавляется в список entryComponents и предварительно компилируется.

schemas

Схемы — это способ определить, как Angular компилирует шаблоны и выдает ли ошибку, когда находит элементы, которые не являются стандартными HTML или известными компонентами. По умолчанию Angular выдает ошибку, когда находит в шаблоне элемент, которого он не знает, но вы можете изменить это поведение, установив для схемы либо NO_ERRORS_SCHEMA (чтобы разрешить все элементы и свойства), либо CUSTOM_ELEMENTS_SCHEMA (чтобы разрешить любые элементы или свойства с - в их имени).

id

Это свойство позволяет вам присвоить NgModule уникальный идентификатор, который вы можете использовать для получения ссылки на фабрику модулей. Это редкий случай использования в настоящее время.

Примеры NgModule

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

Особенности NgModules

Самый базовый вариант использования для NgModules, кроме AppModule предназначен для Feature NgModules (обычно называемых функциональными модулями, но пытающимися сохранить условия единообразными). Они помогают разделить отдельные части вашего приложения и настоятельно рекомендуются. В большинстве случаев они совпадают с основным приложением NgModule. Давайте посмотрим на базовый функциональный модуль NgModule:

 @NgModule({ declarations: [ ForumComponent, ForumsComponent, ThreadComponent, ThreadsComponent ], imports: [ CommonModule, FormsModule, ], exports: [ ForumsComponent ] providers: [ ForumsService ] }) export class ForumsModule { } 

Эта простая функция NgModule определяет четыре компонента, одного поставщика, и импортирует два модуля, которые требуются компонентам и сервису. Вместе они составляют необходимые части для раздела форумов приложения.

Элементы в providers доступны для любого NgModule, который импортирует ForumsModule для инъекции, но важно понимать, что каждый NgModule получит свой экземпляр этой службы. Это отличается от поставщиков, перечисленных в корневом модуле NgModule, от которого вы всегда будете получать один и тот же экземпляр (если только он не предоставлен). Здесь важно понимать внедрение зависимостей, в частности иерархическое внедрение зависимостей . Легко представить, что вы получите тот же экземпляр службы и измените его свойства, но никогда не увидите изменений в другом месте приложения.

Как мы узнали ранее, элементы в declarations фактически недоступны для использования в других NgModules, потому что они являются частными для этого NgModule. Чтобы это исправить, вы можете при желании экспортировать те объявления, которые вы хотите использовать в других модулях Ng, например, в этом фрагменте, где он экспортирует только ForumsComponent . Теперь в любой другой функциональной версии NgModules вы можете поместить <app-forums></app-forums> forums <app-forums></app-forums> (или любой другой селектор для компонента), чтобы отобразить ForumsComponent в шаблоне.

Другое ключевое отличие состоит в том, что ForumsModule импортирует CommonModule вместо BrowserModule . Модуль BrowserModule должен быть импортирован только в корневой NgModule, но CommonModule содержит основные угловые директивы и каналы (такие как NgFor и Date pipe). Если ваш функциональный модуль NgModule не использует ни одну из этих функций, он на самом деле не нуждается в CommonModule .

Теперь, когда вы хотите использовать ForumsModule в своем проекте, вам нужно импортировать его в свой AppModule как вы видите здесь:

 @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, ForumsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } 

Этот NgModule затем импортируется в основной AppModule для его правильной загрузки, который включает в себя элементы в ForumsModule поставщиков ForumsModule и любые экспортированные элементы для потребления в вашем приложении.

Когда вы используете Angular CLI, вы можете легко сгенерировать Feature NgModules, запустив генератор для нового NgModule:

 ng generate module path/to/module/feature 

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

Ленивая загрузка модулей Ng с маршрутами

Иногда вы хотите загружать код только тогда, когда он нужен пользователю, и в Angular это в настоящее время возможно при совместном использовании маршрутизатора и Feature NgModules. Маршрутизатор имеет возможность ленивой загрузки NgModules, когда пользователь запрашивает определенный маршрут. Посмотрите этот учебник по маршрутизации с Angular, если вы новичок в маршрутизации.

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

Нет никакой разницы в том, как вы определяете сам NgModule, за исключением того, что вам нужно определить некоторые маршруты с RouterModule.forChild() . У вас должен быть один маршрут с пустым путем, который будет действовать как корневой маршрут для этого функционального модуля NgModule, а все другие маршруты зависают от него:

 @NgModule({ declarations: [ ForumComponent, ForumsComponent, ], imports: [ CommonModule, FormsModule, RouterModule.forChild([ {path: '', component: ForumsComponent}, {path: ':forum_id', component: ForumComponent} ]) ], providers: [ ForumsService ] }) export class ForumsModule { } 

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

Чтобы загрузить ленивый маршрут, основной AppModule определяет путь, который идет к этому Feature NgModule. Для этого вам необходимо обновить конфигурацию корневого маршрутизатора для нового маршрута. В этом примере показано, как определить ленивый загруженный маршрут, задав ему свойства path и loadChildren :

 const routes: Routes = [ { path: 'forums', loadChildren: 'app/forums/forums.module#ForumsModule' }, { path: '', component: HomeComponent } ]; 

Синтаксис свойства loadChildren представляет собой строку, содержащую путь к файлу NgModule (без расширения файла), символ # , а затем имя класса loadChildren: 'path/to/module#ModuleName . Angular использует это, чтобы знать, куда загружать файл во время выполнения, и знать имя NgModule.

Путь к ленивому загруженному маршруту определяется на корневом уровне маршрутов, поэтому ленивый загруженный NgModule даже не знает конкретно, каким будет путь для его маршрута. Это делает их более пригодными для повторного использования и позволяет приложению знать, когда лениво загружать этот NgModule. Подумайте о ленивом загруженном модуле NgModule, определяющем все маршруты как относительные пути, а полный путь обеспечивается путем объединения корневого маршрута и лениво загруженных маршрутов.

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

Маршрутизация NgModules

Обычный шаблон для Angular — это использование отдельного NgModule для размещения всех ваших маршрутов. Это сделано для разделения проблем, и это совершенно необязательно. Angular CLI поддерживает автоматическую генерацию модуля маршрутизации NgModule при создании нового модуля путем передачи флага --routing :

 ng generate module path/to/module/feature --routing 

Происходит следующее: вы создаете автономный NgModule, который определяет ваши маршруты, а затем ваш Feature NgModule импортирует его. Вот как может выглядеть NgModule для маршрутизации:

 const routes: Routes = [ { path: '', component: ForumsComponent } ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class ForumsRoutingModule { } 

Затем вы просто импортируете его в свой ForumsModule как видите здесь:

 @NgModule({ declarations: [ ForumComponent, ForumsComponent, ], imports: [ CommonModule, FormsModule, ForumsRoutingModule, ], providers: [ ForumsService ] }) export class ForumsModule { } 

Это в значительной степени предпочтение, но это общий шаблон, который вы должны рассмотреть. По сути, это еще один способ использования NgModules для разделения кода.

Синглтон услуги

Мы видели пару проблем, связанных с провайдерами, в которых нельзя было гарантировать, что вы получите один и тот же экземпляр службы через NgModules, если только вы не предоставили его в корневом NgModule. Есть способ определить ваш NgModule так, чтобы он мог объявлять провайдеров только для корневого NgModule, но не объявлять их повторно для всех других NgModule.

Фактически, Angular router является хорошим примером этого. Когда вы определяете маршрут в своем корневом NgModule, вы используете RouterModule.forRoot(routes) , но внутри Feature NgModules вы используете RouterModule.forChild(routes) . Этот шаблон является общим для любой повторно используемой библиотеки, которой требуется один экземпляр службы (singleton). Мы можем сделать то же самое с любым NgModule, добавив два статических метода в наш NgModule, как вы видите здесь:

 @NgModule({ declarations: [ ForumComponent, ForumsComponent, ThreadComponent, ThreadsComponent ], imports: [ CommonModule, FormsModule, ], exports: [ ForumsComponent ] }) export class ForumsModule { static forRoot(): ModuleWithProviders { return { ngModule: ForumsModule, providers: [ForumsService] }; } static forChild(): ModuleWithProviders { return { ngModule: ForumsModule, providers: [] }; } } 

Затем в нашем AppModule вы определяете импорт с помощью forRoot() , который возвращает NgModule с провайдерами. В любом другом NgModule, который импортирует ForumsModule , вы бы использовали метод forChild() чтобы вы больше не объявляли провайдера (таким образом создавая новый экземпляр):

 @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, ForumsModule.forRoot() ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } 

NgModules для группировки NgModules

Вы можете объединить несколько других модулей NgModle в один, чтобы упростить импорт и повторное использование. Например, в проекте Clarity, над которым я работаю, у нас есть несколько NgModules, которые экспортируют только другие NgModules. Например, это основной ClarityModule который фактически реэкспортирует другие отдельные модули NgModule, содержащие каждый из компонентов:

 @NgModule({ exports: [ ClrEmphasisModule, ClrDataModule, ClrIconModule, ClrModalModule, ClrLoadingModule, ClrIfExpandModule, ClrConditionalModule, ClrFocusTrapModule, ClrButtonModule, ClrCodeModule, ClrFormsModule, ClrLayoutModule, ClrPopoverModule, ClrWizardModule ] }) export class ClarityModule { } 

Это облегчает импорт сразу нескольких NgModules, но компилятору становится сложнее узнать, какие NgModules используются или нет для оптимизации встряхивания дерева.

Резюме

Мы прошли вихревой тур по NgModules в Angular и рассмотрели ключевые варианты использования. Документация Angular о NgModules также достаточно глубока, и если вы застряли, я предлагаю просмотреть FAQ .