本篇主要介绍 Angular
当中的动态加载相关内容
基本原理 Angular
当中的动态加载主要涉及到以下三个 API
ViewChild
一个属性装饰器,用来从模版视图中获取对应的元素
可以通过模版变量获取,获取的时候可以通过 read
属性设置查询的条件
简单来说就是可以把视图转为不同的实例
ViewContainerRef
ComponentFactoryResolver
一个服务,动态加载组件的核心,这个服务可以将一个组件实例呈现到另一个组件视图上
所以总结起来简单来说就是
特定区域就是一个视图容器,可以通过 ViewChild
来实现获取和查询
然后使用 ComponentFactoryResolver
将『已经声明但是未实例化』的组件解析成可以动态加载的 Component
再将此 Component
呈现到之前的视图容器当中
下面我们就来看看具体的实现
动态加载已经声明的组件 引用的是 Angular 组件基础内容 当中的示例,模版文件十分简洁,就是通过创建一个 #dyncomp
句柄,以便获得引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 import { Component, OnInit, ViewChild, ViewContainerRef, ComponentFactoryResolver, ComponentRef } from '@angular/core' import { Child11Component } from './child11/child11.component' import { state } from '@angular/animations' @Component ({ selector: 'dynamic-comp' , templateUrl: './dynamic-comp.component.html' , styleUrls: ['./dynamic-comp.component.scss' ] }) export class DynamicCompComponent implements OnInit { @ViewChild ('dyncomp' , { read: ViewContainerRef }) dyncomp: ViewContainerRef comp1: ComponentRef<Child11Component> comp2: ComponentRef<Child11Component> constructor ( private resolver: ComponentFactoryResolver ) { } ngOnInit() { } ngAfterContentInit() { console .log(`动态创建组件的实例` ) const childComp = this .resolver.resolveComponentFactory(Child11Component) this .comp1 = this .dyncomp.createComponent(childComp) this .comp2 = this .dyncomp.createComponent(childComp, 0 ) this .comp2.instance.title = `第二个子组件` } public destoryChild(): void { this .comp1.destroy() this .comp2.destroy() } }
上面特别需要注意的一点就是,对于动态加载的组件必须要声明在特性模块的 entryComponents
中,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @NgModule ({ declarations: [ AppComponent, DynamicCompComponent, Child11Component ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent], entryComponents: [Child11Component] }) export class AppModule { }
也就是说对于此处声明的组件,Angular
都会创建一个 ComponentFactory
并将其存储在 ComponentFactoryResolve
中,也就是动态加载必须的步骤
动态加载还未创建的组件 在上面我们实现了对于已经创建好的组件的动态加载,那么如果是还未创建的组件呢?其实简单来说这种情况就是我们需要『动态创建』不存在的组件而不是已经声明的组件,不过这种情况一般很少遇见,但是如果遇到可以考虑使用 Compiler
,它作用就是用于在运行的时候运行 Angular
编译器来创建 ComponentFactory
的服务,然后可以使用它来创建和呈现组件实例
我们知道,容器创建和呈现组件的函数需要一个 ComponentFactory
,而 Compiler
能够在运行的时候动态创建一个 ComponentFactory
,而有了 ComponentFactory
以后,我们就可以使用上面的方式来进行动态加载了,下面是一个简单的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import { Compiler } from '@angular/core' constructor ( private cp: Compiler ) {}this .comp1 = this .dyncomp.createComponent(this .createModule())createModule() { @Component ({ template: '动态组件' }) class DynamicCom {} @NgModule ({ declarations: [ DynamicCom ] }) class DynamicModule { } return this .cp.compileModuleAndAllComponentsSync(DynamicModule).componentFactories .find(comFac => comFac.componentType === DynamicCom) }
引申出来的问题 在实际开发过程当中,通常的情况下我们不可能仅仅创建了一个动态组件就丢在那里不管它了,一般来说都会有数据的传递,比如要进行更新之类的操作,那么这里就可能会遇到在创建动态组件后,调用 componentRef.instance
后发现不能更新界面数据,下面就是我们尝试在动态组件创建了以后,使用动态组件的实例 componentRef.instance
去改变动态组件的属性,如下
1 componentRef.instance.name = '123'
运行以后可以发现,界面上绑定的 name
属性并不会更新,但是控制台输出发现其中的 name
已经变更,并且如果是在动态组件当中使用函数,可以改变 name
属性的值,并且 name
属性也是可是实时更新,但是如果使用的是 componentRef
,这样数据实时更新就不会起作用了,在这种情况下,有两种解决方法,一种是使用 setTimeout
,可以解决问题,但是并不怎么优雅
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Component ({ template: ` 我是测试模板 {{data}} ` }) export class CustomComponent implements OnInit { @Input () public data: string public ngOnInit() { console .log(this .data) setTimeout(() => { console .log(this .data) }, 3000 ) } } export class HomeComponent implements OnInit { constructor ( private viewContainerRef: ViewContainerRef, private cfr: ComponentFactoryResolver ) { } public ngOnInit() { let factory = this .cfr.resolveComponentFactory(CustomComponent) let componentRef = this .viewContainerRef.createComponent(factory) componentRef.instance.data = 'hello' setTimeout(() => { componentRef.instance.data = 'bye' }, 2000 ) } }
第二种解决方式可以采用官方提供的 changeDetectorRef.markForCheck()
来解决这个问题,即手动去触发检测更新,详细见 ChangeDetectorRef
宿主对象 上面介绍了 Angular
中动态加载的一些相关内容,下面来看一些宿主对象相关的知识点,主要包括
Angular
中利用指令来指定宿主对象
Angular
中如何动态添加宿主
如何与动态添加后的组件进行通信
下面我们就一个一个来进行了解
使用指令来指定宿主对象 在 Angular
中,我们通常需要一个宿主(Host
)来给动态加载的组件提供一个容器,这个宿主在 Angular
中就是 ng-template
,我们需要找到组件中的容器,并且将目标组件加载到这个宿主中,就需要通过创建一个指令(Directive
)来对容器进行标记,先来看看模版文件
1 2 3 4 5 6 <h1 > {{title}} </h1 > <ng-template dl-host > <ng-template >
然后我们添加一个用于标记这个属性的指令 dl-host.directive
1 2 3 4 5 6 7 8 9 10 11 12 import { Directive, ViewContainerRef } from '@angular/core' @Directive ({ selector: '[dl-host]' }) export class DlHostDirective { constructor (public viewContainerRef: ViewContainerRef ) { } }
这样一来,我们就可以在 app.component
中通过 @ViewChild
获取到 dl-host
的实例,因此进而获取到其中的 ViewContainerRef
,另外,我们还需要为 ViewContainerRef
提供需要创建组件 A
的工厂,所以还需要在 app.component
中注入一个工厂生成器 ComponentFactoryResolver
,并且在 app.module
中将需要生成的组件注册为一个 @NgModule.entryComponent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { Component, ViewChild, ComponentFactoryResolver } from '@angular/core' import { DlHostDirective } from './dl-host.directive' import { AComponent } from './a/a.component' @Component ({ selector: 'app-root' , templateUrl: './app.component.html' , styleUrls: ['./app.component.css' ] }) export class AppComponent { title = 'app works!' @ViewChild (DlHostDirective) dlHost: DlHostDirective constructor (private componentFactoryResolver: ComponentFactoryResolver ) { } ngAfterViewInit() { this .dlHost.viewContainerRef.createComponent( this .componentFactoryResolver.resolveComponentFactory(AComponent) ) } }
下面是模块内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { BrowserModule } from '@angular/platform-browser' import { NgModule } from '@angular/core' import { AppComponent } from './app.component' import { AComponent } from './a/a.component' import { DlHostDirective } from './dl-host.directive' @NgModule ({ declarations: [AppComponent, AComponent, DlHostDirective], imports: [BrowserModule, FormsModule, HttpModule], entryComponents: [AComponent], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
如何动态添加宿主 我们不可能在每一个需要动态添加组件的时候提供一个宿主组件,因为我们甚至都不会知道一个组件会在哪儿被创建出来并且被添加到页面中,就比如一个模态窗口,你希望在你需要使用的时候就能打开,而并非受限与宿主,在这种需求的前提下,我们就需要动态添加一个宿主到组件中,所以现在我们可以将 app.component
作为宿主的载体,但是并不提供宿主的显式声明,而是由我们动态去生成宿主,我们先将 app.component
还原
然后我们需要往 DOM
中注入一个节点,例如一个 div
节点作为页面上的宿主,再通过工厂生成一个 AComponent
并将这个组件的根节点添加到宿主上,在这种情况下我们需要通过工厂直接创建组件,而不是在使用 ComponentContanerRef
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import { Component, ComponentFactoryResolver, Injector, ElementRef, ComponentRef, AfterViewInit, OnDestroy } from '@angular/core' import { AComponent } from './a/a.component' @Component ({ selector: 'app-root' , templateUrl: './app.component.html' , styleUrls: ['./app.component.css' ] }) export class AppComponent implements OnDestroy { title = 'app works!' component: ComponentRef<AComponent> constructor ( private componentFactoryResolver: ComponentFactoryResolver, private elementRef: ElementRef, private injector: Injector ) { this .component = this .componentFactoryResolver .resolveComponentFactory(AComponent) .create(this .injector) } ngAfterViewInit() { let host = document .createElement('div' ) host.appendChild((this .component.hostView as any ).rootNodes[0 ]) this .elementRef.nativeElement.appendChild(host) } ngOnDestroy() { this .component.destroy() } }
这种手动添加 DOM
的方式会有一个问题,那就是无法对数据进行脏检查,如果修改了 a.component.ts
是不会触发更新的,所以我们需要手动的去通知应用处理这个组件的视图,对这个组件进行脏检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import { Component, ComponentFactoryResolver, Injector, ElementRef, ComponentRef, ApplicationRef, AfterViewInit, OnDestroy } from '@angular/core' import { AComponent } from './a/a.component' @Component ({ selector: 'app-root' , templateUrl: './app.component.html' , styleUrls: ['./app.component.css' ] }) export class AppComponent implements OnDestroy { title = 'app works!' component: ComponentRef<AComponent> constructor ( private componentFactoryResolver: ComponentFactoryResolver, private elementRef: ElementRef, private injector: Injector, private appRef: ApplicationRef ) { this .component = this .componentFactoryResolver .resolveComponentFactory(AComponent) .create(this .injector) appRef.attachView(this .component.hostView) } ngAfterViewInit() { let host = document .createElement("div" ) host.appendChild((this .component.hostView as any ).rootNodes[0 ]) this .elementRef.nativeElement.appendChild(host) } ngOnDestroy() { this .appRef.detachView(this .component.hostView) this .component.destroy() } }
如何与动态添加后的组件进行通信 简单的方法是动态加载的组件通过 @Output()
向外 emit()
事件,外部组件通过监听事件(subscribe
)得到通知
1 2 3 4 <p (click )="onTitleClick()" > {{title}} </p >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { Component, Output, Input, EventEmitter } from '@angular/core' @Component ({ selector: 'app-a' , templateUrl: './a.component.html' , styleUrls: ['./a.component.css' ] }) export class AComponent { @Input () title = 'a works!' @Output () onTitleChange = new EventEmitter<any >() onTitleClick() { this .onTitleChange.emit() } }
下面来看看外部组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import { Component, ComponentFactoryResolver, Injector, ElementRef, ComponentRef, ApplicationRef, AfterViewInit, OnDestroy } from '@angular/core' import { AComponent } from './a/a.component' @Component ({ selector: 'app-root' , templateUrl: './app.component.html' , styleUrls: ['./app.component.css' ] }) export class AppComponent implements OnDestroy { title = 'app works!' component: ComponentRef<AComponent> constructor ( private componentFactoryResolver: ComponentFactoryResolver, private elementRef: ElementRef, private injector: Injector, private appRef: ApplicationRef ) { this .component = this .componentFactoryResolver .resolveComponentFactory(AComponent) .create(this .injector) appRef.attachView(this .component.hostView) (<AComponent>this .component.instance).onTitleChange .subscribe(() => { console .log('title clicked' ) }) (<AComponent>this .component.instance).title = 'a works again!' } ngAfterViewInit() { let host = document .createElement('div' ) host.appendChild((this .component.hostView as any ).rootNodes[0 ]) this .elementRef.nativeElement.appendChild(host) } ngOnDestroy() { this .appRef.detachView(this .component.hostView) this .component.destroy() } }
查看页面可以看到界面就显示了 a works again!
的文字,点击这行文字,就可以看到在 console
中输入了 title clicked