Статья на английском языке README.md
Приложение Angular состоит из модулей, в которых хранятся компоненты, директивы, службы и так далее. Со временем, в приложение добавляется новый функционал и увеличивается количество его модулей. Как следствие - увеличивается общее время его загрузки. Для сокращения времени загрузки приложения можно применить асинхронную маршрутизацию (lazy load). Асинхронная загрузка позволяет загружать модули в момент обращения пользователя к пункту глобального меню (маршруту).
Создадим приложение, в котором будет несколько функциональных модулей. Каждый такой модуль будем называть доменным и в нем будет сосредоточена функциональность по работе с определенной сущностью. Эти доменные модули будут загружаться отложенной загрузкой (lm - loadable modules).
Создать каталог для проекта перейти в него:
$ mkdir /home/alexey/ws_ts3/crm-simple5 && cd /home/alexey/ws_ts3/crm-simple5
Установить локально требуемую версию @angular/cli (использовалась версия Angular 10):
$ npm install @angular/cli@10
Можно установить локально последнюю версию:
$ npm install @angular/cli@latest
В результате в текущем каталоге появляется новый подкаталог node_modules
, в котором содержится требуемая версия @angular/cli
.
Выполнить создание рабочего пространства и основного приложения crm-simple:
$ npx ng new crm-simple --directory=. --routing=true --style=scss
ng new crm-simple
- создать новое приложение--directory=.
- в текущем каталоге--routing=true
- генерировать модуль routing--style=scss
- использовать preprocessor 'scss'
Библиотека Angular Material содержит много компонент, которые нам потребуются. С описанием этой библиотеки можно ознакомится на сайте https://material.angular.io/.
Добавим в проект библиотеку Angular Material версии 10, так как был установлен Angular версии 10.
$ npx ng add @angular/material@10
На все вопросы отвечаем по умолчанию.
Добавим в наше приложение доменный модуль по работе с клиентами. Этот модуль будет грузиться отложенной загрузкой по маршруту lm-client. Данный модуль является промежуточным доменным модулем, так после него должен загружаться основной доменный модуль.
$ npx ng generate module lm-client --routing=true --route=lm-client --module=app-routing.module
--routing=true
- генерировать модуль routing.--route=lm-client
- наименование маршрута для модуля с отложенной загрузкой. Создает компонент в новом модуле и добавляет маршрут к этому компоненту вRoutes
, указанного в модуле опции--module
.--module=app-routing.module
- модуль в массивRoutes
которого добавляет маршрут к новому компоненту.
Создадим общий заголовок для списка клиентов и свойств клиента компонент c-header.
$ npx ng generate module lm-client/c-header
$ npx ng generate component lm-client/c-header --export=true
Выделим сервис по клиентам client-api в отдельный библиотечный модуль lib-client.
$ npx ng generate module lib-client
Создадим интерфейс для дата-транспорт объекта по данным о клиенте.
$ npx ng generate interface lib-client/_interface/client-dto interface
Создадим сервис для взаимодействия с сервером по данным клиента.
$ npx ng generate service lib-client/_services/client-api
Создадим сервис для работы с данными клиента. В этом сервисе будет некоторая бизнес логика.
$ npx ng generate service lib-client/_services/client
Пока у нас нет BackEnd создадим перехватчик для имитации ответов сервера. И так как модуль HttpClientModule добавлен в список импортов в основном модуле AppModule, то и перехватчик то же должен там быть.
$ npx ng generate interceptor _interceptors/mock-client --skipTests=true
Создадим компонент для отображения списка клиентов.
$ npx ng generate module lib-client/client-grid
$ npx ng generate component lib-client/client-grid --export=true
Создадим модуль и компонент для отображения списка клиентов. Этот модуль будет грузиться отложенной загрузкой по маршруту 'list'.
$ npx ng generate module lm-client/client-list --route=list --module=lm-client-routing.module
Создадим модуль и компонент для отображения свойств клиентов. Этот модуль будет грузиться отложенной загрузкой по маршруту 'view/:clientId'.
$ npx ng generate module lm-client/client-view --route=view/:clientId --module=lm-client-routing.module
Создадим модуль и компонент c-view для отображения вкладок с информацией по данном клиенте.
$ npx ng generate module lm-client/c-view
$ npx ng generate component lm-client/c-view --export=true
Создадим модуль и компонент c-view-info для отображения информации о свойствах лиента.
$ npx ng generate module lm-client/c-view-info
$ npx ng generate component lm-client/c-view-info --export=true
Создадим модуль и компонент c-view-task-list для отображения информации о связанных задачах.
$ npx ng generate module lm-client/c-view-task-list
$ npx ng generate component lm-client/c-view-task-list --export=true
Добавим в наше приложение доменный модуль по работе с задачами. Этот модуль будет грузиться по отложенной загрузке по маршруту lm-task.
$ npx ng generate module lm-task --routing=true --route=lm-task --module=app-routing.module
-
--routing=true
- генерировать модуль routing. -
--route=lm-task
- наименование маршрута для модуля с отложенной загрузкой. Создает компонент в новом модуле и добавляет маршрут к этому компоненту вRoutes
, указанного в модуле опции--module
. -
--module=app-routing.module
- модуль в массивRoutes
которого добавляет маршрут к новому компоненту.
При сборке модуля, в него попадают все используемые сущности (классы, сервисы, компоненты и так далее). Оба модуля используют сервис получения данных о клиентах client-api.service. Этот сервис находится в отдельном библиотечном модуле lib-client.module. И модуль lib-client.module указан в списке импорта в обоих модулях: client-list.module, client-view.module.
Опишем сервис client-api.service.ts в списке провайдеров модуля lib-client.module.ts.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MD_NAME, MD_COLOR } from './_consts/lib-client.consts';
import { ClientApiService } from './_services/client-api.service';
import { ClientService } from './_services/client.service';
@NgModule({
declarations: [],
imports: [
CommonModule,
],
providers: [
ClientApiService,
ClientService
]
})
export class LibClientModule {
constructor() {
console.log(MD_NAME + 'LibClientModule();', MD_COLOR);
}
}
Результат загрузки маршрута /lm-client/list.
В консоли видно, что перед созданием компонента client-list.component выполняется создание нашего сервиса client-api.service.
Результат загрузки маршрута /lm-task.
В консоли видно, что перед созданием компонента lm-task.component выполняется повторное создание нашего сервиса client-api.service. При повторной загрузке модуля lib-client.module выполняется повторное создание сервиса client-api.service.
Если планируется использовать сервис для передачи данных из одного модуля в другой, то такой вариант не подходит.
Выполним доработку модуля lib-client.module таким образом, что бы при повторной его загрузке сервис client-api.service повторно не создавался.
import { NgModule, SkipSelf, Optional } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { MD_NAME, MD_COLOR } from './_consts/lib-client.consts';
import { ClientApiService } from './_services/client-api.service';
import { ClientService } from './_services/client.service';
export const CLIENT_API_SERVICE_FACTORY =
(parentService: ClientApiService, http: HttpClient): ClientApiService => {
return parentService || new ClientApiService(http);
};
export const CLIENT_SERVICE_FACTORY =
(parentService: ClientService, clientApiService: ClientApiService): ClientService => {
return parentService || new ClientService(clientApiService);
};
@NgModule({
declarations: [],
imports: [
CommonModule,
],
providers: [
{
provide: ClientApiService,
deps: [[new Optional(), new SkipSelf(), ClientApiService], HttpClient],
useFactory: CLIENT_API_SERVICE_FACTORY
},
{
provide: ClientService,
deps: [[new Optional(), new SkipSelf(), ClientService], ClientApiService],
useFactory: CLIENT_SERVICE_FACTORY
}
]
})
export class LibClientModule {
constructor() {
console.log(MD_NAME + 'LibClientModule();', MD_COLOR);
}
}
В разделе провайдеры описываем новый провайдер с типом ClientApiService, для создания которого используется фабричная функция CLIENT_API_SERVICE_FACTORY. При создании этой фабричной функции передается экземпляр ClientApiService. Если экземпляр ClientApiService уже существует, то именно он и будет возвращаться. Если экземпляра ClientApiService еще нет, то он будет создан.
Из документации angular.io/api/core:
Optional Декоратор параметра, который будет использоваться для параметров конструктора, который отмечает параметр как необязательную зависимость. Платформа DI предоставляет значение null, если зависимость не обнаружена.
SkipSelf Декоратор параметров, который будет использоваться в параметрах конструктора, который сообщает платформе DI начать разрешение зависимостей из родительского инжектора. Разрешение работает вверх по иерархии инжекторов, поэтому локальный инжектор не проверяется на провайдера.
Так было реализовано в Angular CDK.
Результат загрузки маршрута /lm-client/list.
Результат загрузки маршрута /lm-task.
В консоли видно, что при повторном создании модуля lib-client.module сервис client-api.service повторно не создается, а используется единственный экземпляр данного сервиса.
Создадим общий заголовок для списка задач и свойств задачи компонент t-header.
$ npx ng generate module lm-task/t-header
$ npx ng generate component lm-task/t-header --export=true
Выделим сервис по задачам task-api в отдельный библиотечный модуль lib-task.
$ npx ng generate module lib-task
Создадим интерфейс для дата-транспорт объекта по данным о задаче.
$ npx ng generate interface lib-task/_interfaces/task-dto interface
Создадим сервис для взаимодействия с сервером по данным о задачах.
$ npx ng generate service lib-task/_services/task-api
Создадим сервис для работы с данными о задачах. В этом сервисе будет некоторая бизнес логика.
$ npx ng generate service lib-task/_services/task
Пока у нас нет BackEnd создадим перехватчик для имитации ответов сервера.
$ npx ng generate interceptor _interceptors/mock-task --skipTests=true
Создадим компонент для отображения списка задач.
$ npx ng generate module lib-task/task-grid
$ npx ng generate component lib-task/task-grid --export=true
Создадим модуль и компонент для отображения списка задач. Этот модуль будет грузиться отложенной загрузкой по маршруту 'list'.
$ npx ng generate module lm-task/task-list --route=list --module=lm-task-routing.module
Создадим модуль и компонент для отображения свойств задачи. Этот модуль будет грузиться отложенной загрузкой по маршруту 'view/:taskId'.
$ npx ng generate module lm-task/task-view --route=view/:taskId --module=lm-task-routing.module
Создадим модуль и компонент t-view для отображения вкладок с информацией по данной задаче.
$ npx ng generate module lm-task/t-view
$ npx ng generate component lm-task/t-view --export=true
Создадим модуль и компонент t-view-info для отображения информации о свойствах задачи.
$ npx ng generate module lm-task/t-view-info
$ npx ng generate component lm-task/t-view-info --export=true
При создании доменного модуля, который связан с глобальным пунктом меню, мы рассчитываем, что это будет полностью независимый модуль. Но часто возникает ситуация, когда один доменный модуль использует компоненты другого доменного модуля.
Например, в доменном модуле lm-task имеется компонент для отображения списка задач. И этот же компонент используется в доменном модуле lm-client для отображения списка задач у выбранного клиента. Мы знаем, что при сборке модуля отложенной загрузи в него включаются все компоненты, которые требуются для его работы. Получается, что компонент для отображения списка задач, будет загружаться как при загрузке доменного модуля lm-task, так и при загрузке доменного модуля lm-client. Давайте в этом разберемся.
Выполним сборку проекта:
$ npx ng build
и посмотрим, что получилось в каталоге dist. В данном каталоге мы видим все модули для работы нашего приложения.
client-list-client-list-module.js
!*** ./src/app/lib-client/client-grid/client-grid.module.ts ***!
!*** ./src/app/lm-client/client-list/client-list-routing.module.ts ***!
!*** ./src/app/lib-client/client-grid/client-grid.component.ts ***!
!*** ./src/app/lm-client/client-list/client-list.component.ts ***!
!*** ./src/app/lm-client/client-list/client-list.module.ts ***!
client-view-client-view-module.js
!*** ./src/app/lm-client/c-view-task-list/c-view-task-list.component.ts ***!
!*** ./src/app/lm-client/client-view/client-view-routing.module.ts ***!
!*** ./src/app/lm-client/c-view-info/c-view-info.component.ts ***!
!*** ./src/app/lm-client/client-view/client-view.module.ts ***!
!*** ./src/app/lm-client/c-view/c-view.component.ts ***!
!*** ./src/app/lm-client/c-view-info/c-view-info.module.ts ***!
!*** ./src/app/lm-client/client-view/client-view.component.ts ***!
!*** ./src/app/lm-client/c-view-task-list/c-view-task-list.module.ts ***!
!*** ./src/app/lm-client/c-view/c-view.module.ts ***!
default~client-list-client-list-module~client-view-client-view-module~task-list-task-list-module.js
default~client-list-client-list-module~client-view-client-view-module~task-list-task-list-module~tas~37961fb9.js
default~client-view-client-view-module~task-list-task-list-module.js
!*** ./src/app/lib-task/task-grid/task-grid.component.ts ***!
!*** ./src/app/lib-task/task-grid/task-grid.module.ts ***!
lm-client-lm-client-module.js
!*** ./src/app/lm-client/lm-client-routing.module.ts ***!
!*** ./src/app/lm-client/lm-client.component.ts ***!
!*** ./src/app/lm-client/_consts/lm-client.consts.ts ***!
!*** ./src/app/lm-client/lm-client.module.ts ***!
lm-task-lm-task-module.js
!*** ./src/app/lm-task/_consts/lm-task.consts.ts ***!
!*** ./src/app/lm-task/lm-task.component.ts ***!
!*** ./src/app/lm-task/lm-task-routing.module.ts ***!
!*** ./src/app/lm-task/lm-task.module.ts ***!
task-list-task-list-module.js
!*** ./src/app/lm-task/task-list/task-list.module.ts ***!
!*** ./src/app/lm-task/task-list/task-list-routing.module.ts ***!
!*** ./src/app/lm-task/task-list/task-list.component.ts ***!
task-view-task-view-module.js
!*** ./src/app/lm-task/t-view/t-view.module.ts ***!
!*** ./src/app/lm-task/task-view/task-view.component.ts ***!
!*** ./src/app/lm-task/t-view-info/t-view-info.module.ts ***!
!*** ./src/app/lm-task/task-view/task-view.module.ts ***!
!*** ./src/app/lm-task/t-view/t-view.component.ts ***!
!*** ./src/app/lm-task/task-view/task-view-routing.module.ts ***!
!*** ./src/app/lm-task/t-view-info/t-view-info.component.ts ***!
Как видно, каждый модуль JS содержит сущности, которые в нем используются.
Последовательность загрузки модулей для маршрута /lm-client/3/view/task-list
:
- корневой маршрут app-routing
- по маршруту
/lm-client
загружается модуль lm-client - по маршруту
/view
загружается модуль client-view - по маршруту
/:clientId/task-list
загружается модуль c-view-task-list - загружается компонент c-view-task-list
- используется компонент task-grid
Загрузки модулей для маршрута /lm-task/list
:
- корневой маршрут app-routing
- по маршруту
/lm-task
загружается модуль lm-task - по маршруту
/list
загружается модуль task-list - загружается компонент task-list
- используется компонент task-grid
Рассмотрим схему загрузи модулей нашего проекта.
Мы видим, что в модули: lm-client и lm-task импортируются две библиотеки: lib-client и lib-task. При этом в библиотеке lib-task находится реализация компонента отображения списка задач task-grid, который в дальнейшем используется. И для того, что бы эти библиотеки не загружались повторно они были вынесены оптимизатором в отдельный модуль defaultclient-viewtask-list.
Результат загрузки маршрута /lm-client/view/1/task-list.
Из рисунка видно, что модуль defaultclient-viewtask-list, в котором содержится компонент отображения списка задач task-grid загружен.
Результат загрузки маршрута /lm-task/list.
Из рисунка видно, что когда мы перешли на другой маршрут модуль defaultclient-viewtask-list повторно не загружается.
Вывод: если требуется использовать один компонент в двух доменных модулях, которые загружаются отложенной загрузкой, то необходимо вынести этот компонент в отдельную библиотеку (модуль). И оптимизатор эту библиотеку в отдельный загружаемый модуль. И этот модуль будет загружаться только один раз.
Мы можем не добавлять модули: lib-client и lib-task в список импорта ни в какой другой модуль и при этом все будет работать корректно. Оптимизатор так же создаст модуль defaultclient-viewtask-list, который буде загружаться только один раз.
Исходный код можно скачать github-crm-simple5. (Запустите npm install
перед запуском приложения.)
Запустить проект на сайте StackBlitz можно по ссылке https://stackblitz.com/github/alx-melnichuk/crm-simple5.