美文网首页我爱编程MOS的游乐场
深入Angular:组件(Component)动态加载

深入Angular:组件(Component)动态加载

作者: LordMOS | 来源:发表于2017-06-07 16:26 被阅读0次

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实现框架设计的业务解耦进行展开。尽情期待。

相关文章

  • 深入Angular:组件(Component)动态加载

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

  • 2018-08-30

    Angular4加载顺序 今天遇到了一个比较关于angular4加载组件顺序的坑,当我在app.component...

  • vue中异步组件实现动态挂载

    本篇文章很简单,我们主要想说明两个问题 动态组件Component 和 异步加载组件。 一、常用的组件加载方式 首...

  • Angular组件篇

    Angular组件 一:组件基础 1:什么是组件? 组件(Component)是构成Angular应用的基础和核心...

  • Angular 动态加载组件

    场景:应用在运行期间加载一些新的组件。用法:锚点指令: 需要用锚点指令告诉angular 在哪里插入新组件。首先建...

  • Angular动态加载组件

    引言 有时候需要根据URL来渲染不同组件,我所指的是在同一个URL地址中根据参数的变化显示不同的组件;这是利用An...

  • Angular 动态组件加载

    1.首先定义一个辅助指令 辅助指令用来标记 动态组件 的插入点 2.定义需要动态插入的组件 3.定义Service...

  • angular组件

    Angular组件 @Component装饰器 @Component装饰器用于标示这个类是组件类 selector...

  • Vue Component 组件动态加载

    如果我们希望Vue Component的Html部分可以动态调入,可以使用动态组件技术,组件的定义如下: 需要注意...

  • Angular组件介绍

    1.背景介绍 Angular组件的必备元素 1.组件元数据装饰器:@Component() 告诉angular如何...

网友评论

    本文标题:深入Angular:组件(Component)动态加载

    本文链接:https://www.haomeiwen.com/subject/qjhafxtx.html