深入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 Ive 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-loaderncd dynamic-loadernng g component an
使用ng serve運行這個工程後,我們可以看到一行app works!的文字。如果我們需要在app.comonent中載入a.component,會在app.comonent.html中加入一行(這個selector也是由angular-cli進行生成),在瀏覽器中打開http://localhost:4200,可以看到兩行文字:
app works!na works!n
第二行文字(a.component是由angular-cli進行生成,通常生成的HTML中是a works!)就是組件載入成功的標誌。
Angular如何在組件中動態載入組件
在Angular中,我們通常需要一個宿主(Host)來給動態載入的組件提供一個容器。這個宿主在Angular中就是<ng-template>。我們需要找到組件中的容器,並且將目標組件載入到這個宿主中,就需要通過創建一個指令(Directive)來對容器進行標記。
我們編輯app.comonent.html文件:
app.comonent.html
<h1>n {{title}}n</h1>n<ng-template dl-host></ng-template>n
可以看到,我們在<ng-template>上加入了一個屬性dl-host(為了方便理解,解釋一下這其實就是dynamic-load-host的簡寫),然後我們添加一個用於標記這個屬性的指令dl-host.directive:
dl-host.directive.ts
import { Directive, ViewContainerRef } from @angular/core;n@Directive({ntselector: [dl-host]n})nexport class DlHostDirective {ntconstructor(public viewContainerRef: ViewContainerRef) { }n}n
我們在這裡注入了一個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;nimport { DlHostDirective } from ./dl-host.directive;nimport { AComponent } from ./a/a.component;n@Component({ntselector: app-root,nttemplateUrl: ./app.component.html,ntstyleUrls: [./app.component.css]n})nexport class AppComponent {nttitle = app works!;nt@ViewChild(DlHostDirective) dlHost: DlHostDirective;ntconstructor(private componentFactoryResolver: ComponentFactoryResolver) { }nntngAfterViewInit() {nttthis.dlHost.viewContainerRef.createComponent(ntttthis.componentFactoryResolver.resolveComponentFactory(AComponent)ntt);nt}n}n
app.module.ts
import { BrowserModule } from @angular/platform-browser;nimport { NgModule } from @angular/core;nnimport { AppComponent } from ./app.component;nimport { AComponent } from ./a/a.component;nimport { DlHostDirective } from ./dl-host.directive;nn@NgModule({ntdeclarations: [AppComponent, AComponent, DlHostDirective],ntimports: [BrowserModule, FormsModule, HttpModule],ntentryComponents: [AComponent],ntproviders: [],ntbootstrap: [AppComponent]n})nexport class AppModule { }n
這裡就不得不提到一句什麼是entry component。以下是文檔原文:
An entry component is any component that Angular loads imperatively by type. 所有通過類型進行命令式載入的組件都是入口組件。
這時候我們再去驗證一下,界面展示應該和聲明式載入組件相同。
Angular中如何動態添加宿主
我們不可能在每一個需要動態添加一個宿主組件,因為我們甚至都不會知道一個組件會在哪兒被創建出來並且被添加到頁面中——就比如一個模態窗口,你希望在你需要使用的時候就能打開,而並非受限與宿主。在這種需求的前提下,我們就需要動態添加一個宿主到組件中。
現在,我們將app.component作為宿主的載體,但是並不提供宿主的顯式聲明,我們動態去生成宿主。那麼就先將app.comonent.html文件改回去。
app.comonent.html
<h1>n {{title}}n</h1>n
現在這個界面什麼都沒有了,就只剩下一個標題。那麼接下來我們需要往DOM中注入一個Node,例如一個<div>節點作為頁面上的宿主,再通過工廠生成一個AComponent並將這個組件的根節點添加到宿主上。這種情況下我們需要通過工廠直接創建組件,而不是ComponentContanerRef。
app.comonent.ts
import {n Component, ComponentFactoryResolver, Injector, ElementRef,n ComponentRef, AfterViewInit, OnDestroyn} from @angular/core;nnimport { AComponent } from ./a/a.component;nn@Component({n selector: app-root,n templateUrl: ./app.component.html,n styleUrls: [./app.component.css]n})nnexport class AppComponent implements OnDestroy {n title = app works!;n component: ComponentRef<AComponent>;n n constructor(n private componentFactoryResolver: ComponentFactoryResolver,n private elementRef: ElementRef,n private injector: Injectorn ) {n this.component = this.componentFactoryResolvern .resolveComponentFactory(AComponent)n .create(this.injector);n }nn ngAfterViewInit() {n let host = document.createElement("div");n host.appendChild((this.component.hostView as any).rootNodes[0]);n this.elementRef.nativeElement.appendChild(host);n }n n ngOnDestroy() {n this.component.destroy();n }n}n
這時候我們再去驗證一下,界面展示應該也和聲明式載入組件相同。
但是通過這種方式添加的組件有一個問題,那就是無法對數據進行臟檢查,比如我們對a.component.html以及a.component.ts做點小修改:
a.comonent.html
<p>n {{title}}n</p>n
a.comonent.ts
import { Component } from @angular/core;nn@Component({n selector: app-a,n templateUrl: ./a.component.html,n styleUrls: [./a.component.css]n})nnexport class AComponent {n title = a works!;n}n
這個時候你會發現並不會顯示a works!這行文字。因此我們需要通知應用去處理這個組件的視圖,對這個組件進行臟檢查:
app.comonent.ts
import {n Component, ComponentFactoryResolver, Injector, ElementRef,n ComponentRef, ApplicationRef, AfterViewInit, OnDestroyn} from @angular/core;nnimport { AComponent } from ./a/a.component;nn@Component({n selector: app-root,n templateUrl: ./app.component.html,n styleUrls: [./app.component.css]n})nnexport class AppComponent implements OnDestroy {n title = app works!;n component: ComponentRef<AComponent>;n n constructor(n private componentFactoryResolver: ComponentFactoryResolver,n private elementRef: ElementRef,n private injector: Injector,n private appRef: ApplicationRefn ) {n this.component = this.componentFactoryResolvern .resolveComponentFactory(AComponent)n .create(this.injector);n appRef.attachView(this.component.hostView);n }nn ngAfterViewInit() {n let host = document.createElement("div");n host.appendChild((this.component.hostView as any).rootNodes[0]);n this.elementRef.nativeElement.appendChild(host);n }n n ngOnDestroy() {n this.appRef.detachView(this.component.hostView);n this.component.destroy();n }n}n
如何與動態添加後的組件進行通信
組件間通信在聲明式載入組件中通常直接寫在了組件的屬性中:[]表示@Input,()表示@Output,動態載入組件也是同理。比如我們期望通過外部傳入a.component的title,並在title被單擊後由外部可以知道。所以我們先對動態載入的組件本身進行修改:
a.comonent.html
<p (click)="onTitleClick()">n {{title}}n</p>n
a.comonent.ts
import { Component, Output, Input, EventEmitter } from @angular/core;nn@Component({n selector: app-a,n templateUrl: ./a.component.html,n styleUrls: [./a.component.css]n})nnexport class AComponent {nn @Input() title = a works!;n @Output() onTitleChange = new EventEmitter<any>();n n onTitleClick() {n tthis.onTitleChange.emit();n }n n}n
然後再來修改外部組件:
app.comonent.ts
import {n Component, ComponentFactoryResolver, Injector, ElementRef,n ComponentRef, ApplicationRef, AfterViewInit, OnDestroyn} from @angular/core;nnimport { AComponent } from ./a/a.component;nn@Component({n selector: app-root,n templateUrl: ./app.component.html,n styleUrls: [./app.component.css]n})nnexport class AppComponent implements OnDestroy {n title = app works!;n component: ComponentRef<AComponent>;n n constructor(n private componentFactoryResolver: ComponentFactoryResolver,n private elementRef: ElementRef,n private injector: Injector,n private appRef: ApplicationRefn ) {n this.component = this.componentFactoryResolvern .resolveComponentFactory(AComponent)n .create(this.injector);n appRef.attachView(this.component.hostView);n (<AComponent>this.component.instance).onTitleChangen .subscribe(() => {n console.log("title clicked")n });n (<AComponent>this.component.instance).title = "a works again!";n }nn ngAfterViewInit() {n let host = document.createElement("div");n host.appendChild((this.component.hostView as any).rootNodes[0]);n this.elementRef.nativeElement.appendChild(host);n }n n ngOnDestroy() {n this.appRef.detachView(this.component.hostView);n this.component.destroy();n }n}n
查看頁面可以看到界面就顯示了a works again!的文字,點擊這行文字,就可以看到console中輸入了title clicked。
寫在後面
動態載入這項技術本身的目的是為了完成「框架業務無關化」,在接下來的相關文章中,還會圍繞如何使用Angular實現框架設計的業務解耦進行展開。盡情期待。
推薦閱讀:
※Vue.js起手式+Vue小作品實戰
※Angular Articles 2017-01
※AntV - 我認為這是一個不嚴謹的錯誤
※面向未來的前端數據流框架 - dob
TAG:Angular? | 前端框架 | TypeScript |