动态组件加载器

组件的模板不会永远是固定的。应用可能会需要在运行期间加载一些新的组件。

这本烹饪书为你展示如何使用ComponentFactoryResolver来动态添加组件。

目录

Contents

在线例子 / 可下载的例子查看本烹饪书的源码。

动态组件加载

下面的例子展示了如何构建动态广告条。

英雄管理局正在计划一个广告活动,要在广告条中显示一系列不同的广告。几个不同的小组可能会频繁加入新的广告组件。 再用只支持静态组件结构的模板显然是不现实的。

我们需要一种新的组件加载方式,它不需要在广告条组件的模板中引用固定的组件。

Angular 自带的API就能支持动态加载组件。

指令

在添加组件之前,先要定义一个锚点来告诉Angular要把组件插入到什么地方。

广告条使用一个名叫AdDirective的辅助指令来在模板中标记出有效的插入点。

src/app/ad.directive.ts

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

@Directive({
  selector: '[ad-host]',
})
export class AdDirective {
  constructor(public viewContainerRef: ViewContainerRef) { }
}

AdDirective注入了ViewContainerRef来获取对容器视图的访问权,这个容器就是那些动态加入的组件的宿主。

@Directive装饰器中,要注意选择器的名称:ad-host,它就是我们将应用到元素上的指令。下一节我们会展示如何做。

加载组件

广告条的大部分实现代码都在ad-banner.component.ts中。 为了让这个例子简单点,我们把HTML直接放在了@Component装饰器的template属性中。

<ng-template>元素就是刚才制作的指令将应用到的地方。 要应用AdDirective,回忆一下来自ad.directive.ts的选择器ad-host。把它应用到<ng-template>(不用带方括号)。 这下,Angular就知道该把组件动态加载到哪里了。

src/app/ad-banner.component.ts (template)

template: `
            <div class="ad-banner">
              <h3>Advertisements</h3>
              <ng-template ad-host></ng-template>
            </div>
          `

<ng-template>元素是动态加载组件的最佳选择,因为它不会渲染任何额外的输出。

解析组件

深入看看ad-banner.component.ts中的方法。

AdBannerComponent接收一个AdItem对象的数组作为输入,它最终来自AdServiceAdItem对象指定要加载的组件类,以及绑定到该组件上的任意数据。 AdService可以返回广告活动中的那些广告。

AdBannerComponent传入一个组件数组可以让我们在模板中放入一个广告的动态列表,而不用写死在模板中。

通过getAds()方法,AdBannerComponent可以循环遍历AdItems的数组,并且每三秒调用一次loadComponent()来加载新组件。

src/app/ad-banner.component.ts (excerpt)

export class AdBannerComponent implements AfterViewInit, OnDestroy {
  @Input() ads: AdItem[];
  currentAddIndex: number = -1;
  @ViewChild(AdDirective) adHost: AdDirective;
  subscription: any;
  interval: any;

  constructor(private _componentFactoryResolver: ComponentFactoryResolver) { }

  ngAfterViewInit() {
    this.loadComponent();
    this.getAds();
  }

  ngOnDestroy() {
    clearInterval(this.interval);
  }

  loadComponent() {
    this.currentAddIndex = (this.currentAddIndex + 1) % this.ads.length;
    let adItem = this.ads[this.currentAddIndex];

    let componentFactory = this._componentFactoryResolver.resolveComponentFactory(adItem.component);

    let viewContainerRef = this.adHost.viewContainerRef;
    viewContainerRef.clear();

    let componentRef = viewContainerRef.createComponent(componentFactory);
    (<AdComponent>componentRef.instance).data = adItem.data;
  }

  getAds() {
    this.interval = setInterval(() => {
      this.loadComponent();
    }, 3000);
  }
}

这里的loadComponent()方法很重要。 我们来一步步看看。首先,它选取了一个广告。

loadComponent()如何选择广告

loadComponent()方法使用某种算法选择了一个广告。

(译注:循环选取算法)首先,它把currentAddIndex递增一,然后用它除以AdItem数组长度的余数作为新的currentAddIndex的值, 最后用这个值来从数组中选取一个adItem

loadComponent()选取了一个广告之后,它使用ComponentFactoryResolver来为每个具体的组件解析出一个ComponentFactory。 然后ComponentFactory会为每一个组件创建一个实例。

接下来,我们要把viewContainerRef指向这个组件的现有实例。但我们怎么才能找到这个实例呢? 很简单,因为它指向了adHost,而这个adHost就是我们以前设置过的指令,用来告诉Angular该把动态组件插入到什么位置。

回忆一下,AdDirective曾在它的构造函数中注入了一个ViewContainerRef。 因此这个指令可以访问到这个被我们用作动态组件宿主的元素。

要把这个组件添加到模板中,我们可以调用ViewContainerRefcreateComponent()

createComponent()方法返回一个引用,指向这个刚刚加载的组件。 使用这个引用就可以与该组件进行交互,比如设置它的属性或调用它的方法。

对选择器的引用

通常,Angular编译器会为模板中所引用的每个组件都生成一个ComponentFactory类。 但是,对于动态加载的组件,模板中不会出现对它们的选择器的引用。

要想确保编译器照常生成工厂类,就要把这些动态加载的组件添加到NgModuleentryComponents数组中:

src/app/app.module.ts (entry components)

entryComponents: [ HeroJobAdComponent, HeroProfileComponent ],

公共的AdComponent接口

在广告条中,所有组件都实现了一个公共接口AdComponent,它定义了一个标准化的API,让我们把数据传给组件。

下面就是两个范例组件及其AdComponent接口:

  1. import { Component, Input } from '@angular/core';
  2. import { AdComponent } from './ad.component';
  3. @Component({
  4. template: `
  5. <div class="job-ad">
  6. <h4>{{data.headline}}</h4>
  7. {{data.body}}
  8. </div>
  9. `
  10. })
  11. export class HeroJobAdComponent implements AdComponent {
  12. @Input() data: any;
  13. }

最终的广告栏

最终的广告栏是这样的:

Ads

参见在线例子 / 可下载的例子