深入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阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容