Что нужно знать о динамических компонентах Angular

внешний интерфейс переводчик JavaScript Angular.js

Оригинальная ссылка:Here is what you need to know about dynamic components in Angular

Create Components Dynamically

В этой статье объясняется, как динамически создать компонент (Примечание. Компоненты, используемые в шаблоне, можно назвать статически созданными компонентами) в Angular.

Если вы раньше программировали на AngularJS (фреймворк Angular первого поколения), вы можете использовать$compileСервис генерирует HTML и подключается к модели данных для двусторонней привязки:

const template = '<span>generated on the fly: {{name}}</span>'
const linkFn = $compile(template);
const dataModel = $scope.$new();
dataModel.name = 'dynamic';

// link data model to a template
linkFn(dataModel);

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

Посмотрев некоторое время на внутренний код Angular, я обнаружил, что этот недавно разработанный фреймворк придает большое значение производительности.Вы часто найдете эти предложения в исходном коде Angular (Примечание: для ясности, без перевода):

Attention: Adding fields to this is performance sensitive!

Note: We use one type for all nodes so that loops that loop over all nodes of a ViewDefinition stay monomorphic!

For performance reasons, we want to check and update the list every five seconds.

Итак, дизайнеры Angular решили пожертвовать гибкостью ради огромного прироста производительности, такого как внедрение JIT и компилятора AOT, статических шаблонов, фабрик директив/модулей (ComponentFactory), заводской парсер (ComponentFactoryResolver). Эти концепции являются иностранными и даже враждебными для сообщества Angularjs, но не волнуйтесь, если вы слышали только об этих концепциях, и теперь захотите узнать, что они, продолжайте читать эту статью, и вы будете взорваться.

Примечание. На самом деле компилятор JIT/AOT говорит об одном и том же компиляторе, но этот компилятор используется только во время выполнения или на этапе сборки.

Что касается фабрики, компилятор Angular компилирует написанные вами компоненты, такие как a.component.ts, в a.component.ngfactory.js, то есть компилятор использует декоратор @Component в качестве исходного материала для компиляции написанных вами классов компонентов/директив. в другой вид заводской вид.

Возвращаясь к компилятору JIT/AOT только что, если a.component.ngfactory.js сгенерирован на этапе сборки, это компилятор AOT, и этот компилятор не будет упакован в пакет зависимостей; если он сгенерирован в На этапе выполнения компилятору нужно, чтобы он был упакован в пакет зависимостей и загружался локально пользователем.Во время выполнения компилятор скомпилирует класс компонента/инструкции для создания соответствующего класса фабрики представлений, вот и все. Ниже показано, как выглядит код этих файлов *.ngfactory.js.

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

Фабрики компонентов и компиляторы

Каждый компонент в Angular создается фабрикой компонентов, а фабрика компонентов пишется компилятором в соответствии с вашими требованиями.@ComponentДекоратор в метаданных, генерируемых компилятором. Если вы читаете много онлайн-статей, декоратор еще немного запутался, я могу обратиться написал эту статью средуImplementing custom component decorator.

Angular использует его внутриПосмотретьКонцепция или вся структура — это дерево представлений. Каждое представление состоит из большого количества различных типов узлов (узлов): узлов элементов, текстовых узлов и т. д. (Примечание: вы можете просмотретьМеханизм обновления Angular DOM). Каждый узел имеет свою особую роль, так что обработка каждого узла занимает очень мало времени, и каждый узел имеетViewContainerRefа такжеTemplateRefПодождите службы, вы также можете использоватьViewChild/ViewChildrenа такжеContentChild/ContentChildrenДелайте запросы DOM для этих узлов.

Примечание: Проще говоря, программа Angular представляет собой дерево представлений, каждое представление состоит из нескольких узлов, и каждый узел предоставляет разработчикам API-интерфейс операций шаблона, доступ к которым можно получить через DOM Query API.

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

Предположим, вы написали такой компонент:

@Component({
  selector: 'a-comp',
  template: '<span>A Component</span>'
})
class AComponent {}

Компилятор генерирует код фабрики компонентов, аналогичный следующему, на основе записанной вами информации, код содержит только важные части (Примечание: весь приведенный ниже код можно понимать какПосмотретьelementDef2а такжеjit_textDef3можно понимать какузел):

function View_AComponent_0(l) {
  return jit_viewDef1(0,[
      elementDef2(0,null,null,1,'span',...),
      jit_textDef3(null,['My name is ',...])
    ]

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

Как вы знаете из вышеизложенного, если вы можете получить доступ к фабрике компонентов, вы можете использовать ее для создания экземпляра соответствующего объекта компонента и использоватьViewContainerRefAPI вставляет компонент/представление в DOM. если ты правViewContainerRefЕсли вы заинтересованы, вы можете проверитьИзучение использования ViewContainerRef в Angular для управления DOM. Как использовать этот API (Примечание. Следующий код показывает, как использоватьViewContainerRefAPI вставляет представление в дерево представлений):

export class SampleComponent implements AfterViewInit {
    @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;

    ngAfterViewInit() {
        this.vc.createComponent(componentFactory);
    }
}

Хорошо, из приведенного выше кода мы можем знать, что пока мы получаем фабрику компонентов, все проблемы будут решены. Теперь вопрос, как получитьComponentFactoryФабрика компонентов, продолжайте читать.

Модули и ComponentFactoryResolver

Хотя в AngularJS также есть модули, ему не хватает фактического пространства имен, необходимого для директив, и есть потенциальные конфликты имен, и пока невозможно инкапсулировать директивы в отдельные модули. Однако, к счастью, Angular усвоил урок и предоставляет соответствующие пространства имен для различных декларативных типов, таких как директивы, компоненты и каналы (примечание: Angular предоставляетModule, используя функцию декоратора@NgModuleУкрасьте класс, чтобы получитьModule).

Как и в Angularjs, компоненты в Angular инкапсулированы в модуль. Компоненты не существуют независимо, если вы хотите использовать другой модуль, вы должны импортировать этот модуль:

@NgModule({
    // imports CommonModule with declared directives like
    // ngIf, ngFor, ngClass etc.
    imports: [CommonModule],
    ...
})
export class SomeModule {}

По той же причине, если модуль хочет предоставить некоторые компоненты другим модулям, вы должны экспортировать эти компоненты, вы можете просмотретьexportsАтрибуты. Например, вы можете просмотретьCommonModuleПрактика исходного кода (Примечание: ViewL24-L25):

const COMMON_DIRECTIVES: Provider[] = [
    NgClass,
    NgComponentOutlet,
    NgForOf,
    NgIf,
    ...
];

@NgModule({
    declarations: [COMMON_DIRECTIVES, ...],
    exports: [COMMON_DIRECTIVES, ...],
    ...
})
export class CommonModule {
}

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

Type X is part of the declarations of 2 modules: ...

Когда Angular компилирует программу, компиляторentryComponentsЗарегистрированные компоненты свойств или компоненты, используемые в шаблонах, компилируются в фабрики компонентов (Примечание: компоненты, используемые во всех статических шаблонах, таких как<a-comp></a-comp>, то есть статический компонент; вentryComponentsОпределенные компоненты, то есть динамические компоненты. Лучшим примером динамических компонентов являетсяAngular Material Dialogкомпоненты, которые можно найти вentryComponentsзарегистрирован вDialogContentCompкомпонент динамически загружает содержимое диалога). ты сможешьSourcesСкомпилированный файл фабрики компонентов виден в теге:

Component Factory

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

export class AppComponent {
  constructor(private resolver: ComponentFactoryResolver) {
    // now the `factory` contains a reference to the BComponent factory
    const factory = this.resolver.resolveComponentFactory(BComponent);
  }

Это в двух компонентахAppComponentа такжеBComponentВсе они определены в модуле или в модуле уже есть компоненты при импорте других модулей.BComponentСоответствующая фабрика компонентов.

Динамически загружать и компилировать модули

Но если компонент определен в другом модуле, и этот модуль загружен по требованию, он закончен? На самом деле, мы все еще можем получить компонентную фабрику компонента, метод такой же, как маршрутизацияloadChildrenМодуль загрузки CI-demand очень похож.

Есть два способа загрузки модулей во время выполнения.первый способэто использоватьSystemJsNgModuleLoaderЗагрузчик модулей, если вы используете загрузчик SystemJS, маршруты также используются при загрузке модулей подмаршрутов.SystemJsNgModuleLoaderкак загрузчик модулей.SystemJsNgModuleLoaderЗагрузчик модулей имеетloadметод для загрузки модуля в браузер, одновременно компилируя модуль и все компоненты, объявленные в модуле.loadМетод должен передать параметр пути к файлу, а также имя экспортируемого модуля, а возвращаемое значениеNgModuleFactory:

loader.load('path/to/file#exportName')

Примечание:NgModuleFactoryИсходный код находится вpackages/core/linkerВ папке код в этой папке в основном粘合剂Код, в основном некоторые классы интерфейса дляCoreМодуль используется, а конкретная реализация находится в других папках.

Если конкретное имя модуля экспорта не указано, загрузчик использует ключевое слово по умолчаниюdefaultИмя экспортируемого модуля. Следует также отметить, что если вы хотите использоватьSystemJsNgModuleLoaderТакже зарегистрируйте его так:

providers: [
    {
      provide: NgModuleFactoryLoader,
      useClass: SystemJsNgModuleLoader
    }
  ]

Конечно вы можетеprovideиспользовать любой токен, но модуль маршрутизации используетNgModuleFactoryLoaderИдентификация, также предпочтительно использовать то же самоеtoken. (Примечание:NgModuleFactoryLoaderЗарегистрируйтесь для просмотра исходного кодаL68, используйте для просмотраL78)

Полный код для загрузки модуля и получения фабрики компонентов выглядит следующим образом:

@Component({
  providers: [
    {
      provide: NgModuleFactoryLoader,
      useClass: SystemJsNgModuleLoader
    }
  ]
})
export class ModuleLoaderComponent {
  constructor(private _injector: Injector,
              private loader: NgModuleFactoryLoader) {
  }

  ngAfterViewInit() {
    this.loader.load('app/t.module#TModule').then((factory) => {
      const module = factory.create(this._injector);
      const r = module.componentFactoryResolver;
      const cmpFactory = r.resolveComponentFactory(AComponent);
      
      // create a component and attach it to the view
      const componentRef = cmpFactory.create(this._injector);
      this.container.insert(componentRef.hostView);
    })
  }
}

но используяSystemJsNgModuleLoaderЕсть еще проблема, приведенный выше кодload()Внутри функции (Примечание: см.L70) на самом деле использует компиляторcompileModuleAsyncметод, который будет использоваться только вentryComponentsКомпоненты, зарегистрированные или используемые в шаблонах компонентов для создания фабрик компонентов. Но если вы просто не хотите регистрировать компонент вentryComponentsВ свойствах доделано? Есть еще решения - пользуйтесьcompileModuleAndAllComponentsAsyncспособ загрузки самого модуля. Этот метод создаст фабрику компонентов для всех компонентов в модуле и вернетModuleWithComponentFactoriesОбъект:

class ModuleWithComponentFactories<T> {
    componentFactories: ComponentFactory<any>[];
    ngModuleFactory: NgModuleFactory<T>;

Следующий код полностью показывает, как использовать этот метод для загрузки модуля и получения фабрик компонентов всех компонентов (Примечание: это то, что я сказал вышевторой способ):

ngAfterViewInit() {
  System.import('app/t.module').then((module) => {
      _compiler.compileModuleAndAllComponentsAsync(module.TModule)
        .then((compiled) => {
          const m = compiled.ngModuleFactory.create(this._injector);
          const factory = compiled.componentFactories[0];
          const cmp = factory.create(this._injector, [], null, m);
        })
    })
}

Однако имейте в виду, что этот метод использует частный API компилятора, вот исходный кодДокументация:

One intentional omission from this list is @angular/compiler, which is currently considered a low level api and is subject to internal changes. These changes will not affect any applications or libraries using the higher-level apis (the command line interface or JIT compilation via @angular/platform-browser-dynamic). Only very specific use-cases require direct access to the compiler API (mostly tooling integration for IDEs, linters, etc). If you are working on this kind of integration, please reach out to us first.

Динамически создавать компоненты во время выполнения

Из вышеизложенного мы знаем, как динамически создавать компоненты с помощью фабрик компонентов в модулях, где модули определяются до выполнения, а модули могут загружаться рано или лениво. Однако вместо того, чтобы определять модули заранее, модули и компоненты можно создавать во время выполнения так же, как в AngularJS.

Сначала посмотрите, как это делает приведенный выше код AngularJS:

const template = '<span>generated on the fly: {{name}}</span>'
const linkFn = $compile(template);
const dataModel = $scope.$new();
dataModel.name = 'dynamic'

// link data model to a template
linkFn(dataModel);

Из приведенного выше кода общий процесс динамического создания представления можно резюмировать следующим образом:

  1. Определите класс компонента и его свойства и украсьте класс компонента декораторами.
  2. Определите класс модуля, объявите класс компонента в классе модуля и используйте декоратор для оформления класса модуля.
  3. Скомпилируйте модуль и все компоненты в модуле, чтобы получить все фабрики компонентов.

Классы модулей — это обычные классы с декораторами модулей, как и классы компонентов, а поскольку декораторы — это простые функции, доступные во время выполнения, мы можем использовать эти декораторы до тех пор, пока они нам нужны, например@NgModule()/@Component()украсить любой класс. Следующий код полностью демонстрирует, как динамически создавать компонент:

@ViewChild('vc', {read: ViewContainerRef}) vc: ViewContainerRef;

constructor(private _compiler: Compiler,
            private _injector: Injector,
            private _m: NgModuleRef<any>) {
}

ngAfterViewInit() {
  const template = '<span>generated on the fly: {{name}}</span>';

  const tmpCmp = Component({template: template})(class {
  });
  const tmpModule = NgModule({declarations: [tmpCmp]})(class {
  });

  this._compiler.compileModuleAndAllComponentsAsync(tmpModule)
    .then((factories) => {
      const f = factories.componentFactories[0];
      const cmpRef = this.vc.createComponent(tmpCmp);
      cmpRef.instance.name = 'dynamic';
    })
}

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

Ahead-of-Time Compilation

Компилятор, упомянутый выше, является компилятором Just-In-Time (JIT), возможно, вы слышали о компиляторе Ahead-of-Time (AOT), на самом деле компилятор Angular только один, они компилируются только в соответствии с устройством. используется на разных этапах и использует разные имена. Если компилятор загружается в браузер, он называется компилятором JIT при использовании во время выполнения; если он используется на этапе компиляции без загрузки в браузер, он называется компилятором AOT при использовании во время компиляции. Использование метода AOT официально рекомендовано Angular и подробно описано в официальной документации.Объяснение причины- Более быстрый рендеринг и меньшие пакеты кода.

Если вы используете AOT, что означает отсутствие компилятора во время выполнения, приведенный выше пример, который не требует компиляции, по-прежнему действителен и может использоваться.ComponentFactoryResolverсделать это, но для динамической компиляции требуется компилятор, так что это не сработает. Однако, если вам нужно использовать динамическую компиляцию, вы должны упаковать компилятор как зависимость для разработки, а затем код загружается в браузер.Это требует некоторых шагов установки, но ничего особенного, посмотрите на код:

import { JitCompilerFactory } from '@angular/compiler';

export function createJitCompiler() {
  return new JitCompilerFactory([{
    useDebug: false,
    useJit: true
  }]).createCompiler();
}

import { AppComponent }  from './app.component';

@NgModule({
  providers: [{provide: Compiler, useFactory: createJitCompiler}],
  ...
})
export class AppModule {
}

В приведенном выше коде мы используем@angular/compilerизJitCompilerFactoryкласс для создания экземпляра фабрики компилятора, а затем определитьCompilerдля регистрации экземпляра фабрики компилятора. Выше приведен весь код, который необходимо изменить. Осталось всего несколько вещей, которые нужно изменить и добавить. Это очень просто, не так ли?

разрушение компонента

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

ngOnDestroy() {
  if(this.cmpRef) {
    this.cmpRef.destroy();
  }
}

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

ngOnChanges

Для всех динамически загружаемых компонентов Angular выполняет обнаружение изменений так же, как и статически загружаемые компоненты, что означает:ngDoCheckТакже называется (Примечание: вы можете просмотреть статью в среднем)Если вы считаете, что ngDoCheck означает, что ваш компонент проверяется — , прочитайте эту статью.). Однако даже если динамически загружаемый компонент объявляет@Inputпривязка ввода, но если свойство привязки ввода родительского компонента изменяется, динамически загружаемый компонентngOnChangesне сработает. Это потому, что это проверяет, что ввод изменяетсяngOnChangesФункция регенерируется только после компиляции компилятором на этапе компиляции.Функция является частью фабрики компонентов и компилируется и генерируется в соответствии с информацией о шаблоне во время компиляции. Поскольку в шаблоне не используется динамически загружаемый компонент, эта функция не будет компилироваться компилятором.

Github

Весь пример кода в этой статье хранится вGithub.

Примечание. В этой статье в основном говорится о компонентахb-compКак динамически загружать компонентыa-comp, если оба находятся в одномmodule, звоните напрямуюComponentFactoryResolverПросто дождитесь API, если не в том жеmodule, просто используйтеSystemJsNgModuleLoaderЗагрузчик модулей подойдет.