最近接手了一個(gè)項(xiàng)目,客戶提出了一個(gè)高大上的需求:要求只有一個(gè)主界面,所有組件通過(guò)Tab來(lái)顯示。其實(shí)這個(gè)需求并不詭異,不喜歡界面跳轉(zhuǎn)的客戶都非常熱衷于這種展現(xiàn)形式。
好吧,客戶至上,搞定它!這種實(shí)現(xiàn)方式在傳統(tǒng)的HTML應(yīng)用中,非常簡(jiǎn)單,只是在這Angular4(以下簡(jiǎn)稱ng)中,咋個(gè)弄呢?
我們先來(lái)了解下ng中動(dòng)態(tài)加載組件的兩種方式:
- 加載已經(jīng)聲明的組件: 使用ComponentFactoryResolver,將一個(gè)組件實(shí)例呈現(xiàn)到另一個(gè)組件視圖上;
- 動(dòng)態(tài)創(chuàng)建組件并加載:使用ComponentFactory和Compiler,創(chuàng)建和呈現(xiàn)組件
根據(jù)我們的需求,各個(gè)組件是事先開(kāi)發(fā)好的,需要在同一個(gè)組件上顯示出來(lái)。所以第一種方式符合我們的要求。
使用ComponentFactoryResolver動(dòng)態(tài)加載組件,需要先了解如下概念:
- ViewChild:屬性裝飾器,通過(guò)它可以獲得視圖上對(duì)應(yīng)的元素;
- ViewContainerRef:視圖容器,可在其上創(chuàng)建、刪除組件;
- ComponentFactoryResolver:組件解析器,可以將一個(gè)組件呈現(xiàn)在另一個(gè)組件的視圖上。
搞明白了概念,看看代碼吧:
//// HTML代碼
<dynamic-container [componentName]="'RoleComponent'" >
</dynamic-container>
//// ts代碼
import {Component, Input, ViewContainerRef, ViewChild,ComponentFactoryResolver,ComponentRef,OnDestroy,OnInit} from '@angular/core';
import {RoleComponent} from "./role/role.component";
@Component({
selector: 'dynamic-container',
entryComponents: [RoleComponent,....], //需要?jiǎng)討B(tài)加載的組件名,這里一定要指定,否則報(bào)錯(cuò)
template: "<ng-template #container></ng-template>"
})
export class DynamicComponent implements OnDestroy,OnInit {
@ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;
@Input() componentName //需要加載的組件名
compRef: ComponentRef<any>; // 加載的組件實(shí)例
constructor(private resolver: ComponentFactoryResolver) {}
loadComponent() {
let factory = this.resolver.resolveComponentFactory(this.componentName);
if (this.compRef) {
this.compRef.destroy();
}
this.compRef = this.container.createComponent(factory) //創(chuàng)建組件
}
ngAfterContentInit() {
this.loadComponent()
}
ngOnDestroy() {
if(this.compRef){
this.compRef.destroy();
}
}
}
代碼的確不復(fù)雜!
可是,如果加載的組件有傳入的參數(shù),比如修改角色組件,需要傳入角色id,該怎么辦呢?有辦法解決,使用ReflectiveInjector(依賴注入),在加載組件時(shí)將需要傳入的參數(shù)注入到組件中。代碼調(diào)整如下:
//// HTML代碼,增加了inputs參數(shù),其值為參數(shù)值對(duì)
<dynamic-container [componentName]="'RoleComponent'" [inputs]="{'myName':'dynamic'}" ></dynamic-container>
//// ts代碼
import { ReflectiveInjector} from '@angular/core';
......
export class DynamicComponent implements OnDestroy,OnInit {
@Input() inputs:any //加載組件需要傳入的參數(shù)組
.......
loadComponent() {
let factory = this.resolver.resolveComponentFactory(this.componentName);
if(!this.inputs)
this.inputs={}
let inputProviders = Object.keys(this.inputs).map((inputName) => {
return {provide: inputName, useValue: this.inputs[inputName]};});
let resolvedInputs = ReflectiveInjector.resolve(inputProviders);
let injector = ReflectiveInjector.fromResolvedProviders(resolvedInputs, this.container.parentInjector);
if (this.compRef) {
this.compRef.destroy();
}
this.compRef = factory.create(injector) //創(chuàng)建帶參數(shù)的組件
this.container.insert(this.compRef.hostView);//呈現(xiàn)組件的視圖
}
ngAfterContentInit() {
this.loadComponent()
}
......
}
////RoleComponent代碼如下
export class RoleComponent implements OnInit {
myName:string
........
constructor(){
//this.myName的值為dynamic
}
}
到此,動(dòng)態(tài)加載組件的界面驕傲滴顯示在界面上。等等,貌似哪里不對(duì)!為什么界面上從后臺(tái)獲取的數(shù)據(jù)沒(méi)有加載?
獲取數(shù)據(jù)的代碼如下:
export class RoleComponent implements OnInit {
roleList=[];
......
constructor(private _roleService.list:RoleService) {
this._roleService.list().subscribe(res=>{
this.roleList=res.roleList;
});
}
......
}
經(jīng)過(guò)反復(fù)測(cè)試,得出結(jié)論如下:從后臺(tái)通過(guò)HTTP獲取的數(shù)據(jù)已經(jīng)獲得,只是沒(méi)有觸發(fā)ng進(jìn)行變更檢測(cè),所以界面沒(méi)有渲染出數(shù)據(jù)。
抱著“遇坑填坑”的信念,研習(xí)ng的文檔,發(fā)現(xiàn)ng支持手動(dòng)觸發(fā)變更檢測(cè),只要在適當(dāng)?shù)奈恢谜{(diào)用變更檢測(cè)即可。同時(shí),ng提供了不同級(jí)別的變更檢測(cè):
- 變更檢測(cè)策略:
Default :ng提供的Default的檢測(cè)策略,只要組件的input發(fā)生改變,就觸發(fā)檢測(cè);
OnPush :OnPush檢測(cè)策略是input發(fā)生改變,并不立即觸發(fā)檢測(cè),而是輸入的引用發(fā)生變化時(shí),才會(huì)觸發(fā)檢測(cè)。 - ChangeDetectorRef.detectChanges():可顯式的控制變更檢測(cè),在需要的地方使用即可;
- NgZone.run():在整個(gè)應(yīng)用中進(jìn)行變更檢測(cè)
- ApplicationRef.tick():在整個(gè)應(yīng)用中進(jìn)行變更檢測(cè),偵聽(tīng)NgZone的onTurnDone事件,來(lái)觸發(fā)檢測(cè)
根據(jù)文檔顯示,ng應(yīng)用缺省就在使用NgZone來(lái)檢測(cè)變更,這對(duì)于正常加載的組件是沒(méi)有問(wèn)題的,但是對(duì)于動(dòng)態(tài)加載的組件卻不起作用。幾次試驗(yàn)下來(lái),唯有第二種方法起作用:顯式調(diào)用ChangeDetectorRef.detectChanges()
于是修改ts代碼:
interval:any
loadComponent() {
......
this.interval=setInterval(() => {
this.compRef.changeDetectorRef.detectChanges();
}, 50); //50毫秒檢測(cè)一次變更
}
ngOnDestroy() {
......
clearInterval(this.interval)
}
鑒于本人的ng技能尚淺,就用這種笨拙的方法解決了數(shù)據(jù)加載問(wèn)題,但是如鯁在喉,總覺(jué)應(yīng)該還有更優(yōu)雅的解決方法,待我再花時(shí)日研究下。
啰嗦至此,文中如有不妥之處,歡迎各位看官指正。
補(bǔ)充一句,強(qiáng)烈推薦PrimeNG,它提供了豐富的前端組件,可以方便取用,大大節(jié)省了界面的開(kāi)發(fā)速度。
參考文獻(xiàn):