Angular4 動態加載組件雜談

最近接手了一個項目,客戶提出了一個高大上的需求:要求只有一個主界面,所有組件通過Tab來顯示。其實這個需求并不詭異,不喜歡界面跳轉的客戶都非常熱衷于這種展現形式。

好吧,客戶至上,搞定它!這種實現方式在傳統的HTML應用中,非常簡單,只是在這Angular4(以下簡稱ng)中,咋個弄呢?

我們先來了解下ng中動態加載組件的兩種方式:

  1. 加載已經聲明的組件: 使用ComponentFactoryResolver,將一個組件實例呈現到另一個組件視圖上;
  2. 動態創建組件并加載:使用ComponentFactory和Compiler,創建和呈現組件

根據我們的需求,各個組件是事先開發好的,需要在同一個組件上顯示出來。所以第一種方式符合我們的要求。

使用ComponentFactoryResolver動態加載組件,需要先了解如下概念:

  1. ViewChild:屬性裝飾器,通過它可以獲得視圖上對應的元素;
  2. ViewContainerRef:視圖容器,可在其上創建、刪除組件;
  3. ComponentFactoryResolver:組件解析器,可以將一個組件呈現在另一個組件的視圖上。

搞明白了概念,看看代碼吧:

    //// 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,....],  //需要動態加載的組件名,這里一定要指定,否則報錯
  template: "<ng-template #container></ng-template>"
})
export class DynamicComponent implements OnDestroy,OnInit {
  @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;
    @Input() componentName      //需要加載的組件名
    compRef: ComponentRef<any>; //  加載的組件實例
    constructor(private resolver: ComponentFactoryResolver) {}
    loadComponent() {
      let factory = this.resolver.resolveComponentFactory(this.componentName);
      if (this.compRef) {
        this.compRef.destroy();
        }
      this.compRef = this.container.createComponent(factory) //創建組件
    }
    ngAfterContentInit() {
      this.loadComponent()
    }
    ngOnDestroy() {
      if(this.compRef){
          this.compRef.destroy();
      }
    }
}

代碼的確不復雜!

可是,如果加載的組件有傳入的參數,比如修改角色組件,需要傳入角色id,該怎么辦呢?有辦法解決,使用ReflectiveInjector(依賴注入),在加載組件時將需要傳入的參數注入到組件中。代碼調整如下:

//// HTML代碼,增加了inputs參數,其值為參數值對
<dynamic-container [componentName]="'RoleComponent'" [inputs]="{'myName':'dynamic'}" ></dynamic-container>

//// ts代碼
import { ReflectiveInjector} from '@angular/core';
......
export class DynamicComponent implements OnDestroy,OnInit {
  
    @Input() inputs:any         //加載組件需要傳入的參數組
    .......
    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) //創建帶參數的組件
      this.container.insert(this.compRef.hostView);//呈現組件的視圖
  }
  ngAfterContentInit() {
    this.loadComponent()
  }
  ......
}
////RoleComponent代碼如下
export class RoleComponent implements OnInit {
    myName:string
    ........
    constructor(){
        //this.myName的值為dynamic
  }
}

到此,動態加載組件的界面驕傲滴顯示在界面上。等等,貌似哪里不對!為什么界面上從后臺獲取的數據沒有加載?

獲取數據的代碼如下:

export class RoleComponent implements OnInit {
  roleList=[];
  ......
  constructor(private _roleService.list:RoleService) {
      this._roleService.list().subscribe(res=>{
          this.roleList=res.roleList;
      });
  }
  ......
}

經過反復測試,得出結論如下:從后臺通過HTTP獲取的數據已經獲得,只是沒有觸發ng進行變更檢測,所以界面沒有渲染出數據。

抱著“遇坑填坑”的信念,研習ng的文檔,發現ng支持手動觸發變更檢測,只要在適當的位置調用變更檢測即可。同時,ng提供了不同級別的變更檢測:

  1. 變更檢測策略:
    Default :ng提供的Default的檢測策略,只要組件的input發生改變,就觸發檢測;
    OnPush :OnPush檢測策略是input發生改變,并不立即觸發檢測,而是輸入的引用發生變化時,才會觸發檢測。
  2. ChangeDetectorRef.detectChanges():可顯式的控制變更檢測,在需要的地方使用即可;
  3. NgZone.run():在整個應用中進行變更檢測
  4. ApplicationRef.tick():在整個應用中進行變更檢測,偵聽NgZone的onTurnDone事件,來觸發檢測

根據文檔顯示,ng應用缺省就在使用NgZone來檢測變更,這對于正常加載的組件是沒有問題的,但是對于動態加載的組件卻不起作用。幾次試驗下來,唯有第二種方法起作用:顯式調用ChangeDetectorRef.detectChanges()

于是修改ts代碼:

interval:any 
loadComponent() {
    ......
    this.interval=setInterval(() => {
      this.compRef.changeDetectorRef.detectChanges();
    }, 50);  //50毫秒檢測一次變更
}
ngOnDestroy() {
    ......
    clearInterval(this.interval)
}

鑒于本人的ng技能尚淺,就用這種笨拙的方法解決了數據加載問題,但是如鯁在喉,總覺應該還有更優雅的解決方法,待我再花時日研究下。

啰嗦至此,文中如有不妥之處,歡迎各位看官指正。

補充一句,強烈推薦PrimeNG,它提供了豐富的前端組件,可以方便取用,大大節省了界面的開發速度。

參考文獻:

  1. Angular 2 Change Detection, Zones and an example
  2. Angular change detection explained
  3. 深入理解Angular2變化監測和ngZone
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,466評論 25 708
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,251評論 4 61
  • 這是回學校的時候,拖了個快要壞掉的行李箱,而且里面裝了很多東西,但他還是很頑強的堅持到宿舍樓下,終于,要上樓的時...
    方舟lsy閱讀 320評論 2 1
  • 不甘于寂寞,卻又害怕熱鬧;想要去遠方看看,但又沒有勇氣獨自前行。我希望自己身旁也有個朋友,他的名字叫小王子——可以...
    微風輕輕暖閱讀 263評論 0 0
  • 佛家一切都講究緣分, 有緣而來,無緣而去。 該來的自然會來, 不該來的盼也無用,求也無益。 一切隨緣,順其自然。 ...
    風情公子閱讀 324評論 0 0