在前端開發過程中,經常會遇到一種情況,根據不同的類型,需要加載不同的元件,一般的做法是通過ngswitch文法來進行類型判斷,在裡面寫元件。例如下面的代碼,會根據不同的報表節點類型加載不同的component來繪制不同的圖表
<div class="height100" [ngSwitch]="node.reportNodeType">
<div class="height100" *ngSwitchCase="reportNodeType.handsonTable">
<hotTable [data]="node.reportNodeValue.value?.data"
[columns]="node.reportNodeValue.value?.columns"
[colHeaders]="node.reportNodeValue.value?.colHeaders"
[options]="node.reportNodeValue.value?.options">
</hotTable>
</div>
<div class="height100" *ngSwitchCase="reportNodeType.bar">
<bi-drill-down-charts #biChart [node]="node" [nodeValue]="node.reportNodeValue" class="height100"
[loading]="node.loading">
</bi-drill-down-charts>
</div>
<div class="height100" *ngSwitchCase="reportNodeType.pie">
<bi-drill-down-charts #biChart [node]="node" class="height100"
[loading]="node.loading">
</bi-drill-down-charts>
</div>
<div class="height100" *ngSwitchDefault>
<bi-charts #biChart [node]="node" class="height100"
[loading]="node.loading"></bi-charts>
</div>
</div>
伴随的問題是随着圖表類型的增加,這裡面的ngSwitchCase語句也會相應增加,變得難以維護;最好的方式是動态的加載需要的元件資訊; 具體的做法:
1>report-node.component.html,report-node.component.ts來封裝報表節點,對外所有使用報表節點的地方,直接引入ReportNodeComponent元件即可,這樣,外界不需要關心到底使用哪種元件,以及如何顯示的内部細節;report-node.component.html内容
<div #container></div>
report-node.component内容
@Component({
selector: 'bi-report-node',
templateUrl: './report-node.component.html',
styles: []
})
export class ReportNodeComponent implements OnInit, OnDestroy {
@Input() node: ReportNode;
@ViewChild('container', {read: ViewContainerRef})
container: ViewContainerRef;
private componentRef: ComponentRef<{}>;
constructor(private componentFactoryResolver: ComponentFactoryResolver,
private reportNodeTypeService: ReportNodeTypeService) {
}
ngOnInit() {
let type = this.node.reportNodeType;
let component = this.reportNodeTypeService.getComponentType(type);
let factory = this.componentFactoryResolver.resolveComponentFactory(component);
this.componentRef = this.container.createComponent(factory);
let instance = <AbstractCommonNodeComponent>this.componentRef.instance;
instance.node = this.node;
}
ngOnDestroy(): void {
if (this.componentRef) {
this.componentRef.destroy();
this.componentRef = null;
}
}
resize(event: INgWidgetEvent) {
if (this.componentRef) {
let nodeComponent = <AbstractCommonNodeComponent>this.componentRef.instance;
nodeComponent.resize(event);
}
}
元件内容中隻有一個container辨別,通過init方法根據節點類型動态建立元件,插入到container辨別div中;
@ViewChild
Angular提供了一種稱為DOM查詢的機制。它以@ViewChild和@ViewChildren裝飾器的形式出現.它們的行為相同,隻有前者傳回一個引用,而後者傳回多個引用作為 QueryList 對象;通常這些裝飾器和模闆引用變量一起工作。模闆引用變量可以了解為Dom元素的引用,可以将它看成html元素id類似的東西。這裡通過ViewContainerRef的createComponent方法,在指定dom元素内插入元件;ViewContainerRef類的createComponent方法
abstract createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], ngModule?: NgModuleRef<any>): ComponentRef<C>;
第一個參數要求插入元件的componentFactory對象,這裡通過
let factory = this.componentFactoryResolver.resolveComponentFactory(component);
來擷取;這裡的component對象我們通過一個服務來擷取,代碼如下
import {Injectable} from '@angular/core';
import {ChartsComponent} from '../../charts/charts.component';
import {DrillDownChartComponent} from '../../charts/drill-down-chart/drill-down-chart.component';
import {HandsonTableNodeComponent} from './handson-table-node/handson-table-node.component';
import {QueryNodeViewComponent} from './query-node-view/query-node-view.component';
@Injectable()
export class ReportNodeTypeService {
private mappings = {
'bar': DrillDownChartComponent,
'pie': DrillDownChartComponent,
'handsonTable': HandsonTableNodeComponent,
"query": QueryNodeViewComponent
};
constructor() {
}
getComponentType(typeName: string) {
let type = this.mappings[typeName];
if (!type) {
type = ChartsComponent;
}
return type;
}
}
定義了一個mapping,然後根據類型找到對應的component;
2>第二個問題,如何向動态生成的component對象中傳遞參數以及互動行為?
abstract createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], ngModule?: NgModuleRef<any>): ComponentRef<C>;
方法的傳回結果是一個component對象的引用,我們可以把動态component元件公用的參數抽象出來 為一個parentComponent,然後把componentRef 轉換為這個parentComponent,将其對其指派即可,比如這裡我們将公用的屬性和操作抽象成一個AbstractCommonNodeComponent,封裝起來,如下所示,其他所有的動态生成的元件繼承該元件
import {Component} from '@angular/core';
import {ReportNode} from '../report-node.model';
import {INgWidgetEvent} from 'ngx-draggable-widget-palan';
export abstract class AbstractCommonNodeComponent {
node: ReportNode;
constructor() {
}
abstract resize(event: INgWidgetEvent);
}
比如ChartsComponent,
import {Component, Input, OnInit} from '@angular/core';
import {DataMartService} from '../entities/data-mart/data-mart.service';
import {NzNotificationService} from 'ng-zorro-antd';
import {ReportNodeService} from '../entities/report-node/report-node.service';
import {AbstractCommonNodeComponent} from '../entities/report-node/abstract-common-node/abstract-common-node.component';
@Component({
selector: 'bi-charts',
templateUrl: './charts.component.html',
styleUrls: [
'charts.scss'
]
})
export class ChartsComponent extends AbstractCommonNodeComponent implements OnInit {
echartsIntance: any;
constructor(public reportNodeService: ReportNodeService,
public notification: NzNotificationService,
public dataMartService: DataMartService) {
super();
}
ngOnInit(): void {
console.log(this.node);
}
private triggerChartSize() {
if (this.echartsIntance) {
this.echartsIntance.resize();
}
}
resize() {
setTimeout(() => {
this.triggerChartSize();
}, 0);
}
onChartInit(ec) {
this.echartsIntance = ec;
}
}
最後我們做了類型強制轉換,然後對其進行指派操作
let instance = <AbstractCommonNodeComponent>this.componentRef.instance;
instance.node = this.node;
總結:這裡用到了抽象工廠設計模式,把所有的動态元件的具體細節封裝起來,對外的元件是ReportNodeComponent,外部不需要知道具體的某種報表節點的具體顯示方式細節;給定一種報表的類型,該元件就通過對應的service(這裡是reportNodeTypeService)來找到對應的元件并渲染出來;之後擴充報表節點的時候,隻需要在reportNodeTypeService中把新的元件加進去就可以。
另外,這些動态元件要在module的entryComponents: [中聲明