[Angular] Day17. Dynamic component loader

介绍完什麽是 template 与 structuarl directive 後,接着回来介绍 component 中被跳过的章节,那就是动态仔入 component,再开发专案时可能会遇到 component template 需要被动态载入的情况,比如说常见的网页广告或是当卷轴转到某个地方时才会显示出只定 component 的 template,那麽就继续看下去吧。

https://ithelp.ithome.com.tw/upload/images/20210818/20124767jPdUbmDQQh.png


Dynamic component loading

以 Angular 官方文档的例子来介绍一下如何使用 Dynamic component loading,这个例子将会制作一个画面中的广告,会随着时间而显示不同的广告内容,要满足这个条件使用过去静态 component 载入就显得不切实际,让我们一起看看这个例子吧


The anchor directive

在添加动态 component 之前,需要先使用 directive 来让 Angular 知道你要将这的动态 component 插入在哪边,使用 Angular CLI 建立一个 directive

ng generate directive Ad

接着在 ad.directive.ts 中从 @angular/core 中引入 ViewContainerRef 并将它注入到 class 中

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

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

ViewContainerRef 让你可以访问动态 component 的 view container。


Loading components

接着我们将要动态显示广告的逻辑定义在 ad-banner 中,所以使用 Angular CLI 建立一个 component

ng generate component ad-banner

接着在 ad.banner.html 中添加 <ng-template> 与使用 directive 绑定这个元素

<div class="ad-banner-example">
  <h3>Advertisements</h3>
  <ng-template adHost></ng-template>
</div>

在 template 中使用 <ng-template> 对於要动态载入 component 的 view 而言非常适合,因为在尚未满足条件的情况下 Angular 并不会将 <ng-template> 的内容放入 DOM 中。


Create view component

在完成 ad-banner 後,接着要建立负责显示广告画面的 component,一样先用 Angular CLI 建立 component

ng generate component hero-job-ad
ng generate component hero-profile

这边的设计比较特别,不像一般传统的 component 设计,他要将这两个 component 做为参数传递给某个 method,藉由这个 method 将 component 实例话而不是像之前的使用他的 selector

import { Component, Input } from '@angular/core';

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

      {{ data.body }}
    </div>
  `,
})
export class HeroJobAdComponent {
  @Input() data: any;
}
import { Component, Input } from '@angular/core';

@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 {
  @Input() data: any;
}

由於不需要使用 selector 所以将不需要的 property 从 meatdata 中移除只留下 template 设定画面。


Create service

在建立完显示画面的 component 後,刚刚提到的要将这两个 component 传给某个 method 将它实例化,那麽就要建立一个 class 用於将他们实例化,这边手动新增一个档案就好

// ad-item.ts

import { Type } from '@angular/core';

export class AdItem {
  constructor(public component: Type<any>, public data: any) {}
}

这边使用了 @angular/core 中的 Type 代表定义的参数的型态是 component 或是其实例,这样才满足我们要将 component 传进这个 class 後实例化的目的。

接着建立一个 service 用於利用刚刚建立出的两个 component 建立画面,首先一样使用 Angular CLI 建立一个 service

ng generate service ad
import { Injectable } from '@angular/core';
import { HeroJobAdComponent } from './view/hero-job-ad/hero-job-ad.component';
import { HeroProfileComponent } from './view/hero-profile/hero-profile.component';

import { AdItem } from './ad-item';

@Injectable({
  providedIn: 'root',
})
export class AdService {
  constructor() {}

  getAds() {
    return [
      new AdItem(HeroProfileComponent, {
        name: 'Bombasto',
        bio: 'Brave as they come',
      }),
      new AdItem(HeroProfileComponent, {
        name: 'Dr IQ',
        bio: 'Smart as they come',
      }),
      new AdItem(HeroJobAdComponent, {
        headline: 'Hiring for several positions',
        body: 'Submit your resume today!',
      }),
      new AdItem(HeroJobAdComponent, {
        headline: 'Openings in all departments',
        body: 'Apply today',
      }),
    ];
  }
}

在 service 中新增一个获取广告的 method,将刚刚建立的 component 作为参数传递给 AdItem 并将 component 需要的 Input 参数也传递进去。


Finish this project

建立完这些工具後,最後要将它们组合起来让画面显示动态的 component 画面,首先先在 app.component.ts 中注入刚刚写的 adService 并调用 method 获得英雄广告的列表

import { Component, OnInit } from '@angular/core';
import { AdService } from './ad.service';
import { AdItem } from './ad-item';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
  ads: AdItem[] = [];
  constructor(private adService: AdService) {}

  ngOnInit() {
    this.ads = this.adService.getAds();
  }
}

接着在 app.component.html 中使用 ad.banner 的 selector 并将 ads 绑定为他的 input binding

<!-- app.component.html -->

<app-ad-banner [ads]="ads"></app-ad-banner>

再来要进到 ad-banner.component.ts 中增加动态 component 的逻辑

import { Component, OnInit, Input, ComponentFactoryResolver, ViewChild, OnDestroy } from '@angular/core';
import { AdItem } from '../ad-item';
import { AdDirective } from '../ad.directive';

@Component({
  selector: 'app-ad-banner',
  templateUrl: './ad-banner.component.html',
})
export class AdBannerComponent implements OnInit, OnDestroy {
  @Input() ads: AdItem[] = [];                                                // (1)
  @ViewChild(AdDirective, {static: true}) adHost!: AdDirective;               // (2)
  interval: any;                                                              // (3)
  currentAdIndex = -1;                                                        // (4)

  constructor(private componentFactoryResolver: ComponentFactoryResolver) { } // (5)

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

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

  loadComponent() {                                                           // (6)
    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<{ data: any }>(componentFactory);
    componentRef.instance.data = adItem.data;
  }

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

这边的逻辑比较复杂一点,来一一说明一下:

  • (1): 利用 @Input() 将 ads 装饰为是父层传递的数据,并使用 AdItem 指定型别。
  • (2): 利用 @ViewChild 将 adHost 装饰为可以访问到 view element 的 property,可以直接在 ad-banner.component 中直接使用 adDirective 中的 method,而将 static 设定为 true 代表在更改检测运行之前会先解析查询的结果。
  • (3): 建立一个 property 用来接收 setInterval 回传的值,主要用於取消计时器。
  • (4): 建立一个 property 用来计算目前要显示第几个英雄广告。
  • (5): 将 ComponentFactoryResolver 注入到 component 中,主要用来将选择的 ad-component 解析为一个 componentFactory
  • (6): 建立一个 method 用於计算要显示第几个英雄广告并将被选中的英雄广告 component 透过 ComponentFactoryResolver 解析为 componentFactory 并将它利用 createComponent 实例化。
  • (7): 建立一个 method 用於建立一个计时器,每过 3 秒就取得一次英雄广告
  • (8): 在 ngObDestory() 中取消计时器

img

在画面中看到每过三秒就会更换一次画面,打开网页中的 conosle 检查一下

img

可以看到每过三秒就会更换一次 component,这就是动态载入 component。


结论

本篇中使用了满多之前提到的技巧来完成这个动态载入 component 的功能,可以一边看 Angular 提供的 stackbitz 一边看我的解释应该会比较好看懂。

明天开始会进入到 Angualr 中非常重要的一个观念,Dependency injection,可能在前面几篇中多多少少都有提到一点关於他的内容,不过没关系之後会详细的讲解他到底是什麽,那我们就明天见吧!


Reference


<<:  3 所以要长怎样?

>>:  [Day02]稽核师的挑战关卡

Day1对於学习Java的看法&安装程序

刚读大一的时候,最让我感到头痛的就是程序设计课了!因为我一直以来都不怎麽喜欢电脑相关的东西,更别说是...

Day28 ATT&CK for ICS - Command and Control

Command and Control 攻击者已经进入工控环境之後,从自己的服务器传送指令给受害主机...

[Day 7] 非监督式学习-降维

非监督式学习-降维 今日学习目标 降维观念 何谓降维? 降维有什麽优点? 常见两种降维方法 PCA ...

[Day 3] 以 Ktor Module 实作模组化开发

Ktor Module Ktor Module 可以用来组织程序码,本身仅是一个 Applicati...

Extra03 - Browserslist - 配置专案执行目标环境

此篇为番外,未收入在本篇是因为 Browserslist 并不是个工具,而是个会常被各种转译器采用...