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
對于一個(gè)系統(tǒng)的框架設(shè)計(jì)來說,業(yè)務(wù)是一種桎梏,如果在框架中做了太多業(yè)務(wù)有關(guān)的事情,那么這個(gè)框架就變得狹隘且難以復(fù)用,它變成了你業(yè)務(wù)邏輯的一部分。在從會寫代碼開始,許多人就在追求代碼上的自由:動態(tài)、按需加載你需要的部分。此時(shí)框架才滿足足夠抽象和需求無關(guān)的這種條件。所以高度抽象的前提是高度動態(tài),今天我們先來聊聊關(guān)于Angular動態(tài)加載組件(這里的所有組件均指Component,下同)相關(guān)的問題。
Angular如何在組件中聲明式加載組件
在開始之前,我們按照管理,通過angular-cli創(chuàng)建一個(gè)工程,并且生成一個(gè)a組件。
ng new dynamic-loader
cd dynamic-loader
ng g component a
使用ng serve運(yùn)行這個(gè)工程后,我們可以看到一行app works!的文字。如果我們需要在app.comonent中加載a.component,會在app.comonent.html中加入一行<app-a></app-a>(這個(gè)selector也是由angular-cli進(jìn)行生成),在瀏覽器中打開http://localhost:4200,可以看到兩行文字:
app works!
a works!
第二行文字(a.component是由angular-cli進(jìn)行生成,通常生成的HTML中是a works!)就是組件加載成功的標(biāo)志。
Angular如何在組件中動態(tài)加載組件
在Angular中,我們通常需要一個(gè)宿主(Host)來給動態(tài)加載的組件提供一個(gè)容器。這個(gè)宿主在Angular中就是<ng-template>。我們需要找到組件中的容器,并且將目標(biāo)組件加載到這個(gè)宿主中,就需要通過創(chuàng)建一個(gè)指令(Directive)來對容器進(jìn)行標(biāo)記。
我們編輯app.comonent.html文件:
app.comonent.html
<h1>
{{title}}
</h1>
<ng-template dl-host></ng-template>
可以看到,我們在<ng-template>上加入了一個(gè)屬性dl-host(為了方便理解,解釋一下這其實(shí)就是dynamic-load-host的簡寫),然后我們添加一個(gè)用于標(biāo)記這個(gè)屬性的指令dl-host.directive:
dl-host.directive.ts
import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[dl-host]'
})
export class DlHostDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}
我們在這里注入了一個(gè)ViewContainerRef的服務(wù),它的作用就是為組件提供容器,并且提供了一系列的管理這些組件的方法。我們可以在app.component中通過@ViewChild獲取到dl-host的實(shí)例,因此進(jìn)而獲取到其中的ViewContainerRef。另外,我們需要為ViewContainerRef提供需要創(chuàng)建組件A的工廠,所以還需要在app.component中注入一個(gè)工廠生成器ComponentFactoryResolver,并且在app.module中將需要生成的組件注冊為一個(gè)@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.
所有通過類型進(jìn)行命令式加載的組件都是入口組件。
這時(shí)候我們再去驗(yàn)證一下,界面展示應(yīng)該和聲明式加載組件相同。
Angular中如何動態(tài)添加宿主
我們不可能在每一個(gè)需要動態(tài)添加一個(gè)宿主組件,因?yàn)槲覀兩踔炼疾粫酪粋€(gè)組件會在哪兒被創(chuàng)建出來并且被添加到頁面中——就比如一個(gè)模態(tài)窗口,你希望在你需要使用的時(shí)候就能打開,而并非受限與宿主。在這種需求的前提下,我們就需要動態(tài)添加一個(gè)宿主到組件中。
現(xiàn)在,我們將app.component作為宿主的載體,但是并不提供宿主的顯式聲明,我們動態(tài)去生成宿主。那么就先將app.comonent.html文件改回去。
app.comonent.html
<h1>
{{title}}
</h1>
現(xiàn)在這個(gè)界面什么都沒有了,就只剩下一個(gè)標(biāo)題。那么接下來我們需要往DOM中注入一個(gè)Node,例如一個(gè)<div>節(jié)點(diǎn)作為頁面上的宿主,再通過工廠生成一個(gè)AComponent并將這個(gè)組件的根節(jié)點(diǎn)添加到宿主上。這種情況下我們需要通過工廠直接創(chuàng)建組件,而不是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();
}
}
這時(shí)候我們再去驗(yàn)證一下,界面展示應(yīng)該也和聲明式加載組件相同。
但是通過這種方式添加的組件有一個(gè)問題,那就是無法對數(shù)據(jù)進(jìn)行臟檢查,比如我們對a.component.html以及a.component.ts做點(diǎn)小修改:
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!';
}
這個(gè)時(shí)候你會發(fā)現(xiàn)并不會顯示a works!這行文字。因此我們需要通知應(yīng)用去處理這個(gè)組件的視圖,對這個(gè)組件進(jìn)行臟檢查:
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();
}
}
如何與動態(tài)添加后的組件進(jìn)行通信
組件間通信在聲明式加載組件中通常直接寫在了組件的屬性中:[]表示@Input,()表示@Output,動態(tài)加載組件也是同理。比如我們期望通過外部傳入a.component的title,并在title被單擊后由外部可以知道。所以我們先對動態(tài)加載的組件本身進(jìn)行修改:
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!的文字,點(diǎn)擊這行文字,就可以看到console中輸入了title clicked。
寫在后面
動態(tài)加載這項(xiàng)技術(shù)本身的目的是為了完成“框架業(yè)務(wù)無關(guān)化”,在接下來的相關(guān)文章中,還會圍繞如何使用Angular實(shí)現(xiàn)框架設(shè)計(jì)的業(yè)務(wù)解耦進(jìn)行展開。盡情期待。