Skip to content

Latest commit

 

History

History
459 lines (320 loc) · 26.8 KB

README_ru.md

File metadata and controls

459 lines (320 loc) · 26.8 KB

Статья на английском языке README.md

Angular приложение с отложенными подгружаемыми модулями.

Введение

Приложение 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.

Библиотека Angular Material содержит много компонент, которые нам потребуются. С описанием этой библиотеки можно ознакомится на сайте https://material.angular.io/.

Добавим в проект библиотеку Angular Material версии 10, так как был установлен Angular версии 10.

$ npx ng add @angular/material@10

На все вопросы отвечаем по умолчанию.

Создадим загружаемый модуль lm-client для работы с клиентами.

Добавим в наше приложение доменный модуль по работе с клиентами. Этот модуль будет грузиться отложенной загрузкой по маршруту 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

Создадим библиотечный модуль lib-client для общих сущностей по клиентам.

Выделим сервис по клиентам 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

Создадим загружаемый модуль client-list для списка клиентов.

Создадим модуль и компонент для отображения списка клиентов. Этот модуль будет грузиться отложенной загрузкой по маршруту 'list'.

$ npx ng generate module lm-client/client-list --route=list --module=lm-client-routing.module

Создадим загружаемый модуль client-view для свойств клиента.

Создадим модуль и компонент для отображения свойств клиентов. Этот модуль будет грузиться отложенной загрузкой по маршруту '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 для работы с задачами.

Добавим в наше приложение доменный модуль по работе с задачами. Этот модуль будет грузиться по отложенной загрузке по маршруту 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 которого добавляет маршрут к новому компоненту.

Подключение библиотечного модуля lib-client.

При сборке модуля, в него попадают все используемые сущности (классы, сервисы, компоненты и так далее). Оба модуля используют сервис получения данных о клиентах 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.

img5-a-client-list.png

В консоли видно, что перед созданием компонента client-list.component выполняется создание нашего сервиса client-api.service.

Результат загрузки маршрута /lm-task.

img5-a-task.png

В консоли видно, что перед созданием компонента lm-task.component выполняется повторное создание нашего сервиса client-api.service. При повторной загрузке модуля lib-client.module выполняется повторное создание сервиса client-api.service.

Если планируется использовать сервис для передачи данных из одного модуля в другой, то такой вариант не подходит.

Доработка библиотечного модуля lib-client.

Выполним доработку модуля 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.

img5-b-client-list.png

Результат загрузки маршрута /lm-task.

img5-b-task.png

В консоли видно, что при повторном создании модуля lib-client.module сервис client-api.service повторно не создается, а используется единственный экземпляр данного сервиса.

Доработка загружаемого модуля lm-task для работы с задачами.

Создадим общий заголовок для списка задач и свойств задачи компонент t-header.

$ npx ng generate module lm-task/t-header
$ npx ng generate component lm-task/t-header --export=true

Создадим библиотечный модуль lib-task для общих сущностей по задачам.

Выделим сервис по задачам 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

Создадим загружаемый модуль task-list для списка задач.

Создадим модуль и компонент для отображения списка задач. Этот модуль будет грузиться отложенной загрузкой по маршруту 'list'.

$ npx ng generate module lm-task/task-list --route=list --module=lm-task-routing.module

Создадим загружаемый модуль task-view для свойств задачи.

Создадим модуль и компонент для отображения свойств задачи. Этот модуль будет грузиться отложенной загрузкой по маршруту '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

Рассмотрим схему загрузи модулей нашего проекта.

img5-client-view-task-list

Мы видим, что в модули: lm-client и lm-task импортируются две библиотеки: lib-client и lib-task. При этом в библиотеке lib-task находится реализация компонента отображения списка задач task-grid, который в дальнейшем используется. И для того, что бы эти библиотеки не загружались повторно они были вынесены оптимизатором в отдельный модуль defaultclient-viewtask-list.

Результат загрузки маршрута /lm-client/view/1/task-list.

img5-c-client-task-grid.png

Из рисунка видно, что модуль defaultclient-viewtask-list, в котором содержится компонент отображения списка задач task-grid загружен.

Результат загрузки маршрута /lm-task/list.

img5-c-task-task-grid.png

Из рисунка видно, что когда мы перешли на другой маршрут модуль defaultclient-viewtask-list повторно не загружается.

Вывод: если требуется использовать один компонент в двух доменных модулях, которые загружаются отложенной загрузкой, то необходимо вынести этот компонент в отдельную библиотеку (модуль). И оптимизатор эту библиотеку в отдельный загружаемый модуль. И этот модуль будет загружаться только один раз.

Мы можем не добавлять модули: lib-client и lib-task в список импорта ни в какой другой модуль и при этом все будет работать корректно. Оптимизатор так же создаст модуль defaultclient-viewtask-list, который буде загружаться только один раз.

Исходный код можно скачать github-crm-simple5. (Запустите npm install перед запуском приложения.)

Запустить проект на сайте StackBlitz можно по ссылке https://stackblitz.com/github/alx-melnichuk/crm-simple5.