Главная » Статьи » Как Angular разрешает динамические компоненты

Как Angular разрешает динамические компоненты

Как Angular разрешает динамические компоненты

От автора: динамические Angular компоненты – одна из основных концепций, представленных в Angular, они позволяют разработчикам динамически отображать представление компонента во время выполнения. В этой статье объясняется, как класс ComponentFactoryResolver разрешает ComponentFactory. Зачем это знать? Знание того, как работают основные части Angular, поможет лучше понять его, и вы будете увереннее при создании приложений и решении проблем, связанных с ошибками.

Angular Components и Factories

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

@Component({ selector: 'app-root', template: ` <p *ngFor='let fruit of fruits'>{{fruit}}</p> `, styles: []
})
export class AppComponent { title = 'app'; fruits = ['orange','apple'] constructor(){} changeTitle() { this.title = 'Angular app' }
}

Будет скомпилировано в следующее:

function View_AppComponent_0(_l) { return i0.ɵvid(0, [ (_l()(), i0.ɵted(-1, null, ["\n "])), (_l()(), i0.ɵeld(1, 0, null, null, 4, "div", [], null, null, null, null, null)), (_l()(), i0.ɵted(-1, null, ["\n "])), (_l()(), i0.ɵeld(3, 0, null, null, 1, "p", [], null, null, null, null, null)), (_l()(), i0.ɵted(4, null, ["\n ", " works!!\n "])), (_l()(), i0.ɵted(-1, null, ["\n "])), (_l()(), i0.ɵted(-1, null, [" \n "])) ], null, function(_ck, _v) { var _co = _v.component; var currVal_0 = _co.title; _ck(_v, 4, 0, currVal_0); });
}
exports.View_AppComponent_0 = View_AppComponent_0; function View_AppComponent_Host_0(_l) { return i0.ɵvid(0, [ (_l()(), i0.ɵeld(0, 0, null, null, 1, "app-root", [], null, null, null, View_AppComponent_0, RenderType_AppComponent)), i0.ɵdid(1, 49152, null, 0, i2.AppComponent, [], null, null) ], null, null);
}
exports.View_AppComponent_Host_0 = View_AppComponent_Host_0; var AppComponentNgFactory = i0.ɵccf("app-root", i2.AppComponent, View_AppComponent_Host_0, {}, {}, []);
exports.AppComponentNgFactory = AppComponentNgFactory;

В этой статье мы подробно рассмотрим, как Angular разрешает фабрику динамического компонента, используя метод resolveComponentFactory:

const factory = r.resolveComponentFactory(AComponent);
factory.create(injector);

Пример

Допустим, у нас есть ChildComponent:

@Component({
selector: 'child',
template: ` <h1>Child Component</h1>
`,
})
export class ChildComponent {
}

И ParentComponent, который динамически добавляет представление ChildComponent:

@Component({ selector: 'app-root', template: ` <template #parent></template> <button (click)="createChild()">Create Child</button> `,
})
export class ParentComponent { @ViewChild('parent', { read: ViewContainerRef }) container; constructor(private resolver: ComponentFactoryResolver) {} createChild() { this.container.clear(); const factory: ComponentFactory = this.resolver.resolveComponentFactory(ChildComponent); this.componentRef: ComponentRef = this.container.createComponent(factory); }
}

Круто! Элемент template – сюда будет добавлен ChildComponent.

Наиболее важные вещи, которые мы должны здесь отметить, это инъекция ComponentFactoryResolver и разрешение ChildComponent в методе createChild. Когда нажимается кнопка «Create Child», запускается метод createChild, который сначала очищает элемент template, а затем получает фабрику компонентов ChildComponent и создает представление ChildComponent в DOM.

Можно задать следующий вопрос: Что такое ComponentFactoryResolver? И как его метод resolveComponentFactory получает фабрику компонента?

Что такое ComponentFactoryResolver?

ComponentFactoryResolver — это класс в Angular, который хранит фабрики компонентов в хранилище key-value.

--------------- key | value ----------------
[TodoComponent => TodoComponentNgFatory] [AboutComponent => AboutComponentNgFactory]

Он принимает массив типа ComponentFactory, родительский ComponentFactoryResolver и NgModuleRef качестве аргументов для создания экземпляра.

export class CodegenComponentFactoryResolver implements ComponentFactoryResolver { constructor( factories: ComponentFactory<any>[], private _parent: ComponentFactoryResolver, private _ngModule: NgModuleRef<any>) {...}
}

Во время создания объекта массив factories перебирается в цикле и сохраняет каждый компонент ComponentFactory в Map с типом componentType в качестве ключа и фабрикой в качестве значения:

private _factories = new Map<any, ComponentFactory<any>>(); constructor( factories: ComponentFactory<any>[], private _parent: ComponentFactoryResolver, private _ngModule: NgModuleRef<any>) { for (let i = 0; i < factories.length; i++) { const factory = factories[i]; this._factories.set(factory.componentType, factory); } }

Это больше похоже на объект. Он использует механизм key-value для хранения и извлечения значений:

var store = {
//key : value one: 1, two: 2
} // To get the value `1`, we reference with its key `one`
console.log(store.one) // 1 // Value can be stored with its key `three => 3` store['three'] = 3
console.log(store.three) // 3

Как resolveComponentFactory получает фабрику компонента?

Просто, ведь он хранит ее в Map с парой ключ-значение. Все, что он должен сделать, чтобы получить фабрику компонента — это сослаться на нее по ее ключу.

resolveComponentFactory<T>(component: {new (...args: any[]): T}): ComponentFactory<T> { let factory = this._factories.get(component); if (!factory && this._parent) { factory = this._parent.resolveComponentFactory(component); } if (!factory) { throw noComponentFactoryError(component); } return new ComponentFactoryBoundToModule(factory, this._ngModule); }

Метод get используется в Map для получения значения по его ключу. Итак, здесь метод get используется для извлечения ComponentFactory по его ключу (component). Следующий вопрос: как ComponentFactoryResolver получает массив ComponentFactory?

Ранее в статье мы узнали, что компоненты перед выполнением компилируются до кода JS или factories. Компоненты компилируются для генерации ComponentFactory при выполнении, а модули генерируют NgModuleFactory.

Функция createNgModuleFactory используется для генерации NgModuleFactory. AppModule нашего приложения:

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

Его фабрика будет выглядеть так:

var AppModuleNgFactory = i0.ɵcmf(i1.AppModule, [i2.AppComponent], function(_l) { return i0.ɵmod([i0.ɵmpd(512, i0.ComponentFactoryResolver, i0.ɵCodegenComponentFactoryResolver, [ [8, [i3.AppComponentNgFactory]], [3, i0.ComponentFactoryResolver], i0.NgModuleRef ]), i0.ɵmpd(5120, i0.LOCALE_ID, i0.ɵm, [ [3, i0.LOCALE_ID] ]), ... i0.ɵmpd(131584, i0.ApplicationRef, i0.ApplicationRef, [i0.NgZone, i0.ɵConsole, i0.Injector, i0.ErrorHandler, i0.ComponentFactoryResolver, i0.ApplicationInitStatus]), i0.ɵmpd(512, i0.ApplicationModule, i0.ApplicationModule, [i0.ApplicationRef]), i0.ɵmpd(512, i5.BrowserModule, i5.BrowserModule, [ [3, i5.BrowserModule] ]), i0.ɵmpd(512, i6.ɵba, i6.ɵba, []), i0.ɵmpd(512, i6.FormsModule, i6.FormsModule, []), i0.ɵmpd(512, i1.AppModule, i1.AppModule, [])]);
});

Обратите внимание на неясные имена методов, начинающиеся с символа тета ɵ. Так Angular уменьшает размер пакета при минимизации. Этот метод ссылается на реализацию фактического метода, ɵcmf вызывает createNgModuleFactory , ɵmpd => moduleProviderDef , ɵmod => moduleDef.

Мы видим, что createNgModuleFactory принимает в качестве аргументов класс Module, компоненты начальной загрузки и фабрику определения модуля.

export function createNgModuleFactory( ngModuleType: Type<any>, bootstrapComponents: Type<any>[], defFactory: NgModuleDefinitionFactory): NgModuleFactory<any> { return new NgModuleFactory_(ngModuleType, bootstrapComponents, defFactory);
}

Нас больше интересует фабрика определения модуля. NgModuleFactroyDefinition предоставляет контекст, используемый для разрешения компонентов и инъекции зависимостей. Заглянув в него, мы видим, что он вызывает функцию moduleDef, которая передается массивом вызовов moduleProviderDef. moduleProviderDef — это функция, которая определяет, как класс будет храниться и извлекаться через Injector.

export function moduleProvideDef( flags: NodeFlags, token: any, value: any, deps: ([DepFlags, any] | any)[]): NgModuleProviderDef {
... return { // will bet set by the module definition index: -1, deps: depDefs, flags, token, value };
}

Здесь параметр flags указывает, как должно быть разрешено или инстанцировано значение класса/провайдера:

export const enum NodeFlags { ... TypeValueProvider = 1 << 8, TypeClassProvider = 1 << 9, TypeFactoryProvider = 1 << 10, TypeUseExistingProvider = 1 << 11 ...
}

Параметр token: any является ключом, по которому значение класса / провайдера извлекается через Injector. value — фактическое значение token. deps: ([DepFlags, any] | any)[] — это зависимости, используемые для построения экземпляра класса.

moduleProviderDef парсит все это в объект и возвращает его. moduleDef получает массив из него и парсит его в массив providers.

export function moduleDef(providers: NgModuleProviderDef[]): NgModuleDefinition {
... return { // Will be filled later... factory: null, providersByKey, providers };
}

Когда фабрика модулей создается посредством вызова функции createNgModuleFactory, выполняется серия вызовов, которые в конечном итоге приводят к разрешению провайдеров:

export function initNgModule(data: NgModuleData) { const def = data._def; const providers = data._providers = new Array(def.providers.length); for (let i = 0; i < def.providers.length; i++) { const provDef = def.providers[i]; if (!(provDef.flags & NodeFlags.LazyProvider)) { providers[i] = _createProviderInstance(data, provDef); } }
}

Здесь массив провайдеров, сгенерированный функцией moduleDef, перебирается в цикле, и экземпляр каждого провайдера создается и сохраняется в _providers объекта NgModuleDef_:

function _createProviderInstance(ngModule: NgModuleData, providerDef: NgModuleProviderDef): any { let injectable: any; switch (providerDef.flags & NodeFlags.Types) { case NodeFlags.TypeClassProvider: injectable = _createClass(ngModule, providerDef.value, providerDef.deps); break; case NodeFlags.TypeFactoryProvider: injectable = _callFactory(ngModule, providerDef.value, providerDef.deps); break; case NodeFlags.TypeUseExistingProvider: injectable = resolveNgModuleDep(ngModule, providerDef.deps[0]); break; case NodeFlags.TypeValueProvider: injectable = providerDef.value; break; } return injectable === undefined ? UNDEFINED_VALUE : injectable;
}

Мы рассмотрели все это, чтобы узнать, как ComponentFactoryResolver получает свой массив ComponentFactory. Теперь давайте посмотрим на наше определение фабрики модулей.

return i0.ɵmod([i0.ɵmpd(512, i0.ComponentFactoryResolver, i0.ɵCodegenComponentFactoryResolver, [ [8, [i3.AppComponentNgFactory]], [3, i0.ComponentFactoryResolver], i0.NgModuleRef ])

Если посмотреть настройки ComponenetFactoryResolver, мы видим, что здесь передается 512, который обозначает TypeClassProvider в объекте NodeFlags. Это говорит Angular о том, что мы настраиваем провайдер из класса.

Далее i0.ComponentFactoryResolver передается как токен, а i0.ɵCodegenComponentFactoryResolver — как значение. Последний параметр — это зависимости ComponentFactoryResolver.

Помните, когда мы рассмотрели определение ComponentFactoryResolver, мы узнали, что он принимает в свой конструктор в качестве параметров factories: ComponentFactory, private _parent: ComponentFactoryResolver, private _ngModule: NgModuleRef.

Итак, экземпляр ComponentFactoryResolver будет создан следующим образом:

const resolver = new ComponentFactoryResolver([i3.AppComponentNgFactory], i3.ComponentFactoryResolver, i0.NgModuleRef)

Как видно, передается массив ComponentFactory. Таким образом, во время создания объекта происходит перебор массива и сохранение пары ключ-значение каждого компонента ComponentFactory в _factories Map. Поэтому, когда мы делаем это:

... createChild() { ... const factory: ComponentFactory = this.resolver.resolveComponentFactory(ChildComponent); ... }
...

ComponentFactoryResolver перебирает в цикле _factories Map и извлекает ComponentFactory из ключа поставляемого компонента ChildComponent.

Если мы так запустим наше приложение и нажмем кнопку Create Child, будет вызвана ошибка noComponentFactoryError.

Почему? Поскольку ChildComponent не был передан в ComponentFactoryResolver в параметре массива ComponentFactory, он не присутствовал в _factories ключей значений _factories.

Вот почему в уроках, посвященных динамическим компонентам Angular, всегда ссылаются на добавление всех динамически созданных компонентов в свойство entryComponents вашего корневого NgModule (AppModule).

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

Итак, Angular Compiler «знает», они должны быть добавлены к зависимостям ComponentResolverFactory во время компиляции.

var AppModuleNgFactory = i0.ɵcmf(i1.AppModule, [i2.AppComponent], function(_l) { return i0.ɵmod([i0.ɵmpd(512, i0.ComponentFactoryResolver, i0.ɵCodegenComponentFactoryResolver, [ [8, [i3.AppComponentNgFactory, i3.ChildComponentNgFactory]], [3, i0.ComponentFactoryResolver], i0.NgModuleRef ]), ... ]);
});

Теперь, когда настроены зависимости ComponentFactoryResolver, ChildComponentFactory будет присутствовать:

const resolver = new ComponentFactoryResolver([i3.AppComponentNgFactory,i3.ChildComponentNgFactory], i3.ComponentFactoryResolver, i0.NgModuleRef)

Заключение

Мы узнали, как Angular разрешает динамические компоненты. Функция moduleProviderDef используется для настройки провайдера для фреймворка вставки зависимостей Angular.

Именно здесь ComponentFactory динамических компонентов передается классу ComponentFactoryResolver для разрешения фабрики компонента во время выполнения.

Вот и все, теперь мы знаем, что происходит под капотом, когда мы используем метод resolveComponentFactory. Спасибо!!!

Автор: Chidume Nnamdi

Источник: https://blog.bitsrc.io/

Редакция: Команда webformyself.