深入Angular:組件(Component)動態加載

Felt like the weight of the world was on my shoulders…

Pressure to break or retreat at every turn;

Facing the fear that the truth I discovered;

No telling how all this will work out;

But I've come too far to go back now.

~I am looking for freedom,

Looking for freedom…

And to find it cost me everything I have.

Well I am looking for freedom,

Looking for freedom...

And to find it may take everything I have!

—— Freedom by Anthony Hamilton

對于一個系統的框架設計來說,業務是一種桎梏,如果在框架中做了太多業務有關的事情,那么這個框架就變得狹隘且難以復用,它變成了你業務邏輯的一部分。在從會寫代碼開始,許多人就在追求代碼上的自由:動態、按需加載你需要的部分。此時框架才滿足足夠抽象和需求無關的這種條件。所以高度抽象的前提是高度動態,今天我們先來聊聊關于Angular動態加載組件(這里的所有組件均指Component,下同)相關的問題。

Angular如何在組件中聲明式加載組件

在開始之前,我們按照管理,通過angular-cli創建一個工程,并且生成一個a組件。

ng new dynamic-loader
cd dynamic-loader
ng g component a

使用ng serve運行這個工程后,我們可以看到一行app works!的文字。如果我們需要在app.comonent中加載a.component,會在app.comonent.html中加入一行<app-a></app-a>(這個selector也是由angular-cli進行生成),在瀏覽器中打開http://localhost:4200,可以看到兩行文字:

app works!
a works!

第二行文字(a.component是由angular-cli進行生成,通常生成的HTML中是a works!)就是組件加載成功的標志。

Angular如何在組件中動態加載組件

在Angular中,我們通常需要一個宿主(Host)來給動態加載的組件提供一個容器。這個宿主在Angular中就是<ng-template>。我們需要找到組件中的容器,并且將目標組件加載到這個宿主中,就需要通過創建一個指令(Directive)來對容器進行標記。

我們編輯app.comonent.html文件:

app.comonent.html

<h1>
    {{title}}
</h1>
<ng-template dl-host></ng-template>

可以看到,我們在<ng-template>上加入了一個屬性dl-host(為了方便理解,解釋一下這其實就是dynamic-load-host的簡寫),然后我們添加一個用于標記這個屬性的指令dl-host.directive

dl-host.directive.ts

import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
    selector: '[dl-host]'
})
export class DlHostDirective {
    constructor(public viewContainerRef: ViewContainerRef) { }
}

我們在這里注入了一個ViewContainerRef的服務,它的作用就是為組件提供容器,并且提供了一系列的管理這些組件的方法。我們可以在app.component中通過@ViewChild獲取到dl-host的實例,因此進而獲取到其中的ViewContainerRef。另外,我們需要為ViewContainerRef提供需要創建組件A的工廠,所以還需要在app.component中注入一個工廠生成器ComponentFactoryResolver,并且在app.module中將需要生成的組件注冊為一個@NgModule.entryComponent:

app.comonent.ts

import { Component, ViewChild, ComponentFactoryResolver } from '@angular/core';
import { DlHostDirective } from './dl-host.directive';
import { AComponent } from './a/a.component';
@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent {
    title = 'app works!';
    @ViewChild(DlHostDirective) dlHost: DlHostDirective;
    constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
    
    ngAfterViewInit() {
        this.dlHost.viewContainerRef.createComponent(
            this.componentFactoryResolver.resolveComponentFactory(AComponent)
        );
    }
}

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { AComponent } from './a/a.component';
import { DlHostDirective } from './dl-host.directive';

@NgModule({
    declarations: [AppComponent, AComponent, DlHostDirective],
    imports: [BrowserModule, FormsModule, HttpModule],
    entryComponents: [AComponent],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

這里就不得不提到一句什么是entry component。以下是文檔原文:

An entry component is any component that Angular loads imperatively by type.
所有通過類型進行命令式加載的組件都是入口組件。

這時候我們再去驗證一下,界面展示應該和聲明式加載組件相同。

Angular中如何動態添加宿主

我們不可能在每一個需要動態添加一個宿主組件,因為我們甚至都不會知道一個組件會在哪兒被創建出來并且被添加到頁面中——就比如一個模態窗口,你希望在你需要使用的時候就能打開,而并非受限與宿主。在這種需求的前提下,我們就需要動態添加一個宿主到組件中。

現在,我們將app.component作為宿主的載體,但是并不提供宿主的顯式聲明,我們動態去生成宿主。那么就先將app.comonent.html文件改回去。

app.comonent.html

<h1>
    {{title}}
</h1>

現在這個界面什么都沒有了,就只剩下一個標題。那么接下來我們需要往DOM中注入一個Node,例如一個<div>節點作為頁面上的宿主,再通過工廠生成一個AComponent并將這個組件的根節點添加到宿主上。這種情況下我們需要通過工廠直接創建組件,而不是ComponentContanerRef

app.comonent.ts

import {
    Component, ComponentFactoryResolver, Injector, ElementRef,
    ComponentRef, AfterViewInit, OnDestroy
} from '@angular/core';

import { AComponent } from './a/a.component';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})

export class AppComponent implements OnDestroy {
    title = 'app works!';
    component: ComponentRef<AComponent>;
    
    constructor(
        private componentFactoryResolver: ComponentFactoryResolver,
        private elementRef: ElementRef,
        private injector: Injector
    ) {
        this.component = this.componentFactoryResolver
            .resolveComponentFactory(AComponent)
            .create(this.injector);
    }

    ngAfterViewInit() {
        let host = document.createElement("div");
        host.appendChild((this.component.hostView as any).rootNodes[0]);
        this.elementRef.nativeElement.appendChild(host);
    }
    
    ngOnDestroy() {
        this.component.destroy();
    }
}

這時候我們再去驗證一下,界面展示應該也和聲明式加載組件相同。

但是通過這種方式添加的組件有一個問題,那就是無法對數據進行臟檢查,比如我們對a.component.html以及a.component.ts做點小修改:

a.comonent.html

<p>
    {{title}}
</p>

a.comonent.ts

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

@Component({
    selector: 'app-a',
    templateUrl: './a.component.html',
    styleUrls: ['./a.component.css']
})

export class AComponent {
    title = 'a works!';
}

這個時候你會發現并不會顯示a works!這行文字。因此我們需要通知應用去處理這個組件的視圖,對這個組件進行臟檢查:

app.comonent.ts

import {
    Component, ComponentFactoryResolver, Injector, ElementRef,
    ComponentRef, ApplicationRef, AfterViewInit, OnDestroy
} from '@angular/core';

import { AComponent } from './a/a.component';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})

export class AppComponent implements OnDestroy {
    title = 'app works!';
    component: ComponentRef<AComponent>;
    
    constructor(
        private componentFactoryResolver: ComponentFactoryResolver,
        private elementRef: ElementRef,
        private injector: Injector,
        private appRef: ApplicationRef
    ) {
        this.component = this.componentFactoryResolver
            .resolveComponentFactory(AComponent)
            .create(this.injector);
        appRef.attachView(this.component.hostView);
    }

    ngAfterViewInit() {
        let host = document.createElement("div");
        host.appendChild((this.component.hostView as any).rootNodes[0]);
        this.elementRef.nativeElement.appendChild(host);
    }
    
    ngOnDestroy() {
        this.appRef.detachView(this.component.hostView);
        this.component.destroy();
    }
}

如何與動態添加后的組件進行通信

組件間通信在聲明式加載組件中通常直接寫在了組件的屬性中:[]表示@Input()表示@Output,動態加載組件也是同理。比如我們期望通過外部傳入a.componenttitle,并在title被單擊后由外部可以知道。所以我們先對動態加載的組件本身進行修改:

a.comonent.html

<p (click)="onTitleClick()">
    {{title}}
</p>

a.comonent.ts

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

@Component({
    selector: 'app-a',
    templateUrl: './a.component.html',
    styleUrls: ['./a.component.css']
})

export class AComponent {

    @Input() title = 'a works!';
    @Output() onTitleChange = new EventEmitter<any>();
    
    onTitleClick() {
        this.onTitleChange.emit();
    }
    
}

然后再來修改外部組件:

app.comonent.ts

import {
    Component, ComponentFactoryResolver, Injector, ElementRef,
    ComponentRef, ApplicationRef, AfterViewInit, OnDestroy
} from '@angular/core';

import { AComponent } from './a/a.component';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})

export class AppComponent implements OnDestroy {
    title = 'app works!';
    component: ComponentRef<AComponent>;
    
    constructor(
        private componentFactoryResolver: ComponentFactoryResolver,
        private elementRef: ElementRef,
        private injector: Injector,
        private appRef: ApplicationRef
    ) {
        this.component = this.componentFactoryResolver
            .resolveComponentFactory(AComponent)
            .create(this.injector);
        appRef.attachView(this.component.hostView);
        (<AComponent>this.component.instance).onTitleChange
            .subscribe(() => {
                console.log("title clicked")
            });
        (<AComponent>this.component.instance).title = "a works again!";
    }

    ngAfterViewInit() {
        let host = document.createElement("div");
        host.appendChild((this.component.hostView as any).rootNodes[0]);
        this.elementRef.nativeElement.appendChild(host);
    }
    
    ngOnDestroy() {
        this.appRef.detachView(this.component.hostView);
        this.component.destroy();
    }
}

查看頁面可以看到界面就顯示了a works again!的文字,點擊這行文字,就可以看到console中輸入了title clicked

寫在后面

動態加載這項技術本身的目的是為了完成“框架業務無關化”,在接下來的相關文章中,還會圍繞如何使用Angular實現框架設計的業務解耦進行展開。盡情期待。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容