填寫這份《一分鐘調查》,幫我們(開發組)做得更好!去填寫Home

動態元件載入器

Dynamic component loader

譯註:本頁講的是一個用於顯示廣告的範例,而部分廣告攔截器外掛,比如 Chrome 的 AdGuard,可能會破壞其工作邏輯,因此,請在本頁關閉那些外掛。

元件的範本不會永遠是固定的。應用可能會需要在執行期間載入一些新的元件。

Component templates are not always fixed. An application may need to load new components at runtime.

這本烹飪書為你展示如何使用 ComponentFactoryResolver 來動態新增元件。

This cookbook shows you how to use ComponentFactoryResolver to add components dynamically.

現場演練 / 下載範例檢視本烹飪書的原始碼。

See the現場演練 / 下載範例of the code in this cookbook.

動態元件載入

Dynamic component loading

下面的例子展示了如何建構動態廣告條。

The following example shows how to build a dynamic ad banner.

英雄管理局正在計劃一個廣告活動,要在廣告條中顯示一系列不同的廣告。幾個不同的小組可能會頻繁加入新的廣告元件。 再用只支援靜態元件結構的範本顯然是不現實的。

The hero agency is planning an ad campaign with several different ads cycling through the banner. New ad components are added frequently by several different teams. This makes it impractical to use a template with a static component structure.

你需要一種新的元件載入方式,它不需要在廣告條元件的範本中參考固定的元件。

Instead, you need a way to load a new component without a fixed reference to the component in the ad banner's template.

Angular 自帶的 API 就能支援動態載入元件。

Angular comes with its own API for loading components dynamically.

指令

The anchor directive

在新增元件之前,先要定義一個錨點來告訴 Angular 要把元件插入到什麼地方。

Before you can add components you have to define an anchor point to tell Angular where to insert components.

廣告條使用一個名叫 AdDirective 的輔助指令來在範本中標記出有效的插入點。

The ad banner uses a helper directive called AdDirective to mark valid insertion points in the template.

import { Directive, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[adHost]', }) export class AdDirective { constructor(public viewContainerRef: ViewContainerRef) { } }
src/app/ad.directive.ts
      
      import { Directive, ViewContainerRef } from '@angular/core';

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

AdDirective 注入了 ViewContainerRef 來獲取對容器檢視的訪問權,這個容器就是那些動態加入的元件的宿主。

AdDirective injects ViewContainerRef to gain access to the view container of the element that will host the dynamically added component.

@Directive 裝飾器中,要注意選擇器的名稱:ad-host,它就是你將應用到元素上的指令。下一節會展示該如何做。

In the @Directive decorator, notice the selector name, adHost; that's what you use to apply the directive to the element. The next section shows you how.

載入元件

Loading components

廣告條的大部分實現程式碼都在 ad-banner.component.ts 中。 為了讓這個例子簡單點,HTML 被直接放在了 @Component 裝飾器的 template 屬性中。

Most of the ad banner implementation is in ad-banner.component.ts. To keep things simple in this example, the HTML is in the @Component decorator's template property as a template string.

<ng-template> 元素就是剛才製作的指令將應用到的地方。 要應用 AdDirective,回憶一下來自 ad.directive.ts 的選擇器 ad-host。把它應用到 <ng-template>(不用帶方括號)。 這下,Angular 就知道該把元件動態載入到哪裡了。

The <ng-template> element is where you apply the directive you just made. To apply the AdDirective, recall the selector from ad.directive.ts, [adHost]. Apply that to <ng-template> without the square brackets. Now Angular knows where to dynamically load components.

template: ` <div class="ad-banner-example"> <h3>Advertisements</h3> <ng-template adHost></ng-template> </div> `
src/app/ad-banner.component.ts (template)
      
      template: `
            <div class="ad-banner-example">
              <h3>Advertisements</h3>
              <ng-template adHost></ng-template>
            </div>
          `
    

<ng-template> 元素是動態載入元件的最佳選擇,因為它不會渲染任何額外的輸出。

The <ng-template> element is a good choice for dynamic components because it doesn't render any additional output.

解析元件

Resolving components

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

Take a closer look at the methods in ad-banner.component.ts.

AdBannerComponent 接收一個 AdItem 物件的陣列作為輸入,它最終來自 AdServiceAdItem 物件指定要載入的元件類別,以及繫結到該元件上的任意資料。 AdService 可以返回廣告活動中的那些廣告。

AdBannerComponent takes an array of AdItem objects as input, which ultimately comes from AdService. AdItem objects specify the type of component to load and any data to bind to the component.AdService returns the actual ads making up the ad campaign.

AdBannerComponent 傳入一個元件陣列可以在範本中放入一個廣告的動態列表,而不用寫死在範本中。

Passing an array of components to AdBannerComponent allows for a dynamic list of ads without static elements in the template.

透過 getAds() 方法,AdBannerComponent 可以迴圈遍歷 AdItems 的陣列,並且每三秒呼叫一次 loadComponent() 來載入新元件。

With its getAds() method, AdBannerComponent cycles through the array of AdItems and loads a new component every 3 seconds by calling loadComponent().

export class AdBannerComponent implements OnInit, OnDestroy { @Input() ads: AdItem[]; currentAdIndex = -1; @ViewChild(AdDirective, {static: true}) adHost: AdDirective; interval: any; constructor(private componentFactoryResolver: ComponentFactoryResolver) { } ngOnInit() { this.loadComponent(); this.getAds(); } ngOnDestroy() { clearInterval(this.interval); } loadComponent() { this.currentAdIndex = (this.currentAdIndex + 1) % this.ads.length; const adItem = this.ads[this.currentAdIndex]; const componentFactory = this.componentFactoryResolver.resolveComponentFactory(adItem.component); const viewContainerRef = this.adHost.viewContainerRef; viewContainerRef.clear(); const componentRef = viewContainerRef.createComponent<AdComponent>(componentFactory); componentRef.instance.data = adItem.data; } getAds() { this.interval = setInterval(() => { this.loadComponent(); }, 3000); } }
src/app/ad-banner.component.ts (excerpt)
      
      export class AdBannerComponent implements OnInit, OnDestroy {
  @Input() ads: AdItem[];
  currentAdIndex = -1;
  @ViewChild(AdDirective, {static: true}) adHost: AdDirective;
  interval: any;

  constructor(private componentFactoryResolver: ComponentFactoryResolver) { }

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

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

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

    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(adItem.component);

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

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

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

這裡的 loadComponent() 方法很重要。 來一步步看看。首先,它選取了一個廣告。

The loadComponent() method is doing a lot of the heavy lifting here. Take it step by step. First, it picks an ad.

loadComponent() 如何選擇廣告

How loadComponent() chooses an ad

loadComponent() 方法使用某種演算法選擇了一個廣告。

The loadComponent() method chooses an ad using some math.

(譯註:迴圈選取演算法)首先,它把 currentAdIndex 遞增一,然後用它除以 AdItem 陣列長度的餘數作為新的 currentAdIndex 的值, 最後用這個值來從陣列中選取一個 adItem

First, it sets the currentAdIndex by taking whatever it currently is plus one, dividing that by the length of the AdItem array, and using the remainder as the new currentAdIndex value. Then, it uses that value to select an adItem from the array.

loadComponent() 選取了一個廣告之後,它使用 ComponentFactoryResolver 來為每個具體的元件解析出一個 ComponentFactory。 然後 ComponentFactory 會為每一個元件建立一個實例。

After loadComponent() selects an ad, it uses ComponentFactoryResolver to resolve a ComponentFactory for each specific component. The ComponentFactory then creates an instance of each component.

接下來,你要把 viewContainerRef 指向這個元件的現有實例。但你怎麼才能找到這個實例呢? 很簡單,因為它指向了 adHost,而這個 adHost 就是你以前設定過的指令,用來告訴 Angular 該把動態元件插入到什麼位置。

Next, you're targeting the viewContainerRef that exists on this specific instance of the component. How do you know it's this specific instance? Because it's referring to adHost and adHost is the directive you set up earlier to tell Angular where to insert dynamic components.

回憶一下,AdDirective 曾在它的建構函式中注入了一個 ViewContainerRef。 因此這個指令可以訪問到這個你打算用作動態元件宿主的元素。

As you may recall, AdDirective injects ViewContainerRef into its constructor. This is how the directive accesses the element that you want to use to host the dynamic component.

要把這個元件新增到範本中,你可以呼叫 ViewContainerRefcreateComponent()

To add the component to the template, you call createComponent() on ViewContainerRef.

createComponent() 方法返回一個參考,指向這個剛剛載入的元件。 使用這個參考就可以與該元件進行互動,比如設定它的屬性或呼叫它的方法。

The createComponent() method returns a reference to the loaded component. Use that reference to interact with the component by assigning to its properties or calling its methods.

公共的 AdComponent 介面

The AdComponent interface

在廣告條中,所有元件都實現了一個公共介面 AdComponent,它定義了一個標準化的 API,來把資料傳給元件。

In the ad banner, all components implement a common AdComponent interface to standardize the API for passing data to the components.

下面就是兩個範例元件及其 AdComponent 介面:

Here are two sample components and the AdComponent interface for reference:

import { Component, Input } from '@angular/core'; import { AdComponent } from './ad.component'; @Component({ template: ` <div class="job-ad"> <h4>{{data.headline}}</h4> {{data.body}} </div> ` }) export class HeroJobAdComponent implements AdComponent { @Input() data: any; }import { Component, Input } from '@angular/core'; import { AdComponent } from './ad.component'; @Component({ template: ` <div class="hero-profile"> <h3>Featured Hero Profile</h3> <h4>{{data.name}}</h4> <p>{{data.bio}}</p> <strong>Hire this hero today!</strong> </div> ` }) export class HeroProfileComponent implements AdComponent { @Input() data: any; }export interface AdComponent { data: any; }
      
      import { Component, Input } from '@angular/core';

import { AdComponent } from './ad.component';

@Component({
  template: `
    <div class="job-ad">
      <h4>{{data.headline}}</h4>

      {{data.body}}
    </div>
  `
})
export class HeroJobAdComponent implements AdComponent {
  @Input() data: any;

}
    

最終的廣告欄

Final ad banner

最終的廣告欄是這樣的:

The final ad banner looks like this:

參閱現場演練 / 下載範例

See the現場演練 / 下載範例.