Динамические компоненты в любом фреймворке/библиотеке упрощают создание крупномасштабных приложений.

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

Так что нет смысла загружать весь модуль для каждого пользователя, верно? Компоненты должны загружаться по требованию, только если они используются. Цель этой статьи — сделать именно это.

Чтобы упростить задачу, давайте рассмотрим вариант использования. У нас есть приложение с доступом пользователей на основе ролей. Каждый тип пользователя имеет свою страницу профиля и может выполнять различные действия в зависимости от своего типа.

Проблема

Как теперь показать другой компонент для другого типа пользователя?

Одна вещь, которую мы можем сделать, это использовать структурную директиву ngIf для условного отображения определенного компонента на основе типа пользователя.

Но это все равно потребует от нас загрузки обоих компонентов для каждого типа пользователя. Это не имеет никакого смысла, верно?

Решение

Как насчет динамической загрузки требуемого компонента по требованию?

Таким образом, мы не будем загромождать основной пакет обоими этими компонентами и будем загружать только тот компонент, который должен быть отрендерен для конкретного пользователя. Я уверен, что если у вас есть фобия пакетов, вы определенно захотите это сделать 😉.

Как? 🤔

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

Динамический компонент — одна из основных концепций, представленных в Angular. Динамический компонент — это компонент, который создается динамически во время выполнения. У Angular есть свой API для динамической загрузки компонентов.

Разница между двумя вышеуказанными подходами (Источник — неизвестен)

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

  • Компоненты, которые необходимо загружать динамически.
  • Сервис для динамической загрузки и разрешения компонента.
  • Директива для обработки viewContainerRef.

  • Создайте гостевую карту и компонент карты пользователя
  • Реализация сервиса для загрузки и разрешения компонента
  • Создание директивы профиля и компонента профиля
  • Последний шаг, обновление компонента приложения и модуля приложения🤓

Реализация 👩🏻‍💻

Пришло время повеселиться. Начнем со следующих шагов:

  • Откройте cmd и выполните команду ng new dynamic-component
  • Откройте новый проект в выбранном вами редакторе.

Или, если вы просто хотите продолжить, начните с этого Sample StackBlitz.

Приступаем к кодированию 💪

1. Создайте гостевую карту и компонент карты пользователя

Как видно из изображения выше, нам нужны два компонента: карточка для обычного гостевого профиля и одна для зарегистрированного пользователя.

Давайте создадим 2 новых компонента GuestCardComponent и UserCardComponent ClientProfileComponent следующим образом:

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

гостевая карта.component.ts

import { Component } from '@angular/core';
import { ProfileService } from '../profile.service';

@Component({
  selector: 'app-guest-card',
  templateUrl: './guest-card.component.html',
  styleUrls: ['./guest-card.component.css']
})
export class GuestProfileComponent {
  constructor(private profileService: ProfileService) {}

  login() {
    this.profileService.login();
  }

guest-card.component.html

<section class="mt-5 card p-3">
  
    <h2 class="card-title">Guest Profile</h2>

    <p class="card-subtitle">
      Bacon ipsum dolor amet pork belly tri-tip turducken, pancetta bresaola pork chicken meatloaf. Flank sirloin strip steak prosciutto kevin turducken.
    </p>

    <div class="mt-3 text-center mb-3">
      <button class="btn btn-primary" (click)="login()">Login</button>
    </div>
  
</section>

Теперь давайте создадим UserCardComponent Это простая карточка, которая показывает информацию о профиле пользователя.

user-card.component.ts

import { Component } from '@angular/core';
import { ProfileService } from '../profile.service';

@Component({
  selector: 'app-user-card',
  templateUrl: './user-card.component.html',
  styleUrls: ['./user-card.component.css']
})
export class ClientProfileComponent {
  constructor(private profileService: ProfileService) {}

  logout() {
    this.profileService.logout();
  }
}
user-card.component.html
<section class="card p-3 mt-5">
  <figure class="text-center">
    <img src="assets/profile.jpg" />
  </figure>
<div class="card-body">

  <h2 class="card-title" >Lorem ipsum</h2>

  <p class="card-subtitle" >
      Bacon ipsum dolor amet pork belly tri-tip turducken, pancetta bresaola pork chicken meatloaf. Flank sirloin strip steak prosciutto kevin turducken.
  </p>

  <div class="mt-2 text-center">
    <button class="btn btn-danger"(click)="logout()">Logout</button>
  </div>
</div>
</section>

2. Реализация сервиса для загрузки и разрешения компонента

В сервисе мы реализуем метод для загрузки и разрешения компонентов в соответствии с заданным состоянием.

В этом сервисе мы начнем с создания Subject для управления состоянием isLoggedIn и двух методов login и logout для передачи событий в субъект.

И, наконец, волшебный метод🎩

Метод loadComponent принимает состояния ViewContainerRef и isLoggedIn. Чтобы получить ссылку на наш элемент template в компоненте my-app, мы будем использовать ViewContainerRef.

ViewContainerRef:

Как следует из названия, это ссылка на контейнер. ViewContainerRef хранит ссылку на элемент template (наш контейнер, в котором будет размещаться динамически загружаемый компонент), а также предоставляет API для создания компонентов.

Прежде чем мы перейдем к методу createComponent() из ViewContainerRef, нам нужно добавить еще один сервис.

Служба ComponentFactoryResolver предоставляет один основной метод, resolveComponentFactory.

Метод resolveComponentFactory() принимает компонент и возвращает ComponentFactory.

Мы можем думать о ComponentFactory как об объекте, который знает, как создать компонент.

И, наконец, мы просто соберем все вместе внутри метода loadComponent.

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

Как только ViewContainerRef будет очищен, мы добавим условие для получения компонента в соответствии со статусом isLoggedIn, после чего мы передадим компонент в метод toresolveComponentFactory().

После этого мы вызываем метод createComponent(). Внутри этот метод вызовет метод create() из фабрики и добавит компонент в качестве родственного элемента в наш контейнер.

профиль.сервис

import { Injectable,ComponentFactoryResolver, ViewContainerRef } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class ProfileService {
  private isLoggedIn = new BehaviorSubject(false);
  isLoggedIn$ = this.isLoggedIn.asObservable();

  constructor(private cfr: ComponentFactoryResolver) {}

  login() {
    this.isLoggedIn.next(true);
  }

  logout() {
    this.isLoggedIn.next(false);
  }

  async loadComponent(vcr: ViewContainerRef, isLoggedIn: boolean) {
    const { GuestCardComponent } = await import('./guest-card/guest-card.component');

    const { UserCardComponent } = await import('./user-card/user-card.component');

    vcr.clear();
    let component : any = isLoggedIn ? UserCardComponent : GuestCardComponent;
   
    return vcr.createComponent(
      this.cfr.resolveComponentFactory(component))    
}
}

3. Создание директивы профиля и компонента профиля

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

Создадим файл src/app/profile/profile-host.directive.ts:

import { Directive, ViewContainerRef } from '@angular/core';

@Directive({ selector: '[appProfileHost]' })
export class ProfileHostDirective {
  constructor(public viewContainerRef: ViewContainerRef) {}
}

profile-host.directive.ts

Это всего лишь уловка, чтобы упростить получение ViewContainerRef, которое мы ищем.

Теперь давайте создадим ProfileComponent. Мы создадим простой ng-template.

Элемент ‹ng-template› — хороший выбор для динамического компонента, поскольку он не отображает никаких дополнительных выходных данных. мы присоединим ProfileHostDirective к шаблону, чтобы мы могли использовать декоратор ViewChild и получить viewContainerRef.

В методе OnInit мы получаем viewContainerRef и используем наблюдаемую isLoggedIn$ из ProfileService, чтобы знать каждый раз, когда изменяется состояние isLoggedIn. Затем, используя оператор mergeMap, мы вызываем функцию loadComponent, которая творит настоящее волшебство.

import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { ProfileHostDirective } from './profile-host.directive';
import { ProfileService } from './profile.service';
import { mergeMap, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';

@Component({
  selector: 'app-profile-container',
  template: `
    <ng-template appProfileHost></ng-template>
  `
})
export class ProfileComponent implements OnInit, OnDestroy {
  @ViewChild(ProfileHostDirective, { static: true })
  profileHost: ProfileHostDirective;
  private destroySubject = new Subject();

  constructor(private profileService: ProfileService) {}

  ngOnInit() {
    const viewContainerRef = this.profileHost.viewContainerRef;

    this.profileService.isLoggedIn$
      .pipe(
        takeUntil(this.destroySubject),
        mergeMap(isLoggedIn =>
          this.profileService.loadComponent(viewContainerRef, isLoggedIn)
        )
      )
      .subscribe();
  }

  ngOnDestroy() {
    this.destroySubject.next();
    this.destroySubject.complete();
  }
}

profile.component.ts

4. Последний шаг, обновление компонента приложения и модуля приложения🤓

Теперь, когда мы реализовали наши сервисы и компоненты, давайте обновим файлы AppComponent и AppModule.

Мы включим наш ProfileComponent в шаблон AppComponent

<h1 class="header">Dynamic components</h1>
<main class="container">
  <app-profile-container></app-profile-container>
</main>

Теперь единственное, что мы не можем забыть сделать, это добавить ProfileComponent и ProfileHostDirective в массив объявлений AppModule, давайте сделаем это

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { ProfileHostDirective } from './profile/profile-host.directive';
import { ProfileComponent } from './profile/profile.component';

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

— Для отключенных приложений Ivy —

Если Ivy отключен в приложении, для динамически загружаемого компонента и создания ComponentFactory компонент также должен быть добавлен в entryComponents модуля следующим образом:

declarations: [
  AppComponent,
  GuestProfileComponent,
  ClientProfileComponent
],
entryComponents: [GuestProfileComponent,ClientProfileComponent],

Согласно определению entryComponents

Указывает список компонентов, которые должны быть скомпилированы при определении этого модуля. Для каждого перечисленного здесь компонента Angular создаст ComponentFactory и сохранит его в ComponentFactoryResolver.

И Свершилось ✅ . Наше приложение готово к динамической загрузке компонентов.