最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 详解 Angular 动态视图 (一) -- 原生 API

    正文概述 掘金(locotor在掘金)   2021-03-23   808

    动态渲染视图是日常项目开发中常见的需求,特别是通用性的工具库开发。例如弹窗服务,需要让组件的用户决定传入什么内容进行渲染,可能是一个组件,可能是一个模板(ng-template)。这里先来看看通过 Angular API 如何去实现。

    要动态的插入一个视图,Angular 提供了一套 API 负责创建容器,实例化组件,以及管理视图和数据。同时,它还额外提供了一些指令,方便快速的实现。它们各有各自适用场景,本篇详细来看看它们的使用。

    完整的示例代码可以查看 GitHub 项目仓库,或者在线查看效果 在线示例。

    ViewContainerRef 容器

    动态视图的核心是视图容器,它决定了视图的插入位置,用 ViewContainerRef 类表示。要创建它很简单,假设组件视图中有:<ng-container #dynamicHost></ng-container> 通过 @ViewChild 装饰器读取即可:

    @ViewChild('dynamicHost', { read: ViewContainerRef }) container!: ViewContainerRef;
    

    这段代码的注意点有:

    • @ViewChild 用来从组件视图中获取元素的引用,可以是 DOM 对象、子组件或者指令的实例、或是某个依赖注入的 Provider。

    第二个参数中的 read 指示具体获取哪个类型,例如从 <button mat-button> 可以获取到这个 button 元素对象,也可以是这个标签上添加的 Material 按钮指令。

    • 要获取一个容器,并非只能使用 <ng-container><ng-template> 、组件或者别的 DOM 标签都可以。

    • 虽然是叫容器,不过插入的内容可不是在这个 <ng-container> 标签内,而是在它的下方(类似 <router-outlet>)。所以使用 ng-container 是为了不渲染多余的 DOM。

    ViewContainerRef 实例可以创建、插入、移除、移动或是销毁它其中的视图(ViewRef)。视图代表着 Angular中可显示元素的最小分组单位,它由组件或者模板定义。多个视图构造成了 Angular 应用的视图树。

    ✨简明起见,后文都将 ViewContainerRef 实例称之为 “视图容器”

    以代码方式插入

    视图容器有两个方法(createComponent,createEmbeddedView),用来动态创建组件视图和模板视图。

    动态插入组件

    首先创建一个用于动态插入的组件,这个通知组件省略了具体样式,详情可以查看源码。可以注意到,这个组件有一个输入属性,一个输出事件。以便演示动态创建的组件,如何和外界交互。

    @Component({
        template: `
        <div class="alert-container mat-elevation-z2" [class]="classConfig()">
            <span class="message">{{message}}</span>
            <button mat-button (click)="emitCloseEvent()">关闭</button>
        </div>`
    })
    export class AlertComponent {
        @Input() message = '空消息提示';
        @Input() type = 'success';
        @Output() closeAlert = new EventEmitter();
        classConfig() {
            return {
                success: this.type === 'success',
                warning: this.type === 'warning'
            };
        }
    
        const
    
        emitCloseEvent(): void {
            this.closeAlert.emit();
        }
    }
    

    ✨注意:Angular 9 后的版本默认使用 Ivy 编译器,如果是使用老版本编译器,这个需要动态插入的通知组件,需要在 Module 的 entryComponents 中声明,并且这个 Module 不能懒加载。

    通知组件写好后,就可以创建并动态插入,具体分为三步:

    1. 在构造函数中注入ComponentFactoryResolver 实例:private resolver: ComponentFactoryResolver
    2. 通过 resolver.resolveComponentFactory(AlertComponent) 方法,生成这个通知组件的工厂对象。
    3. 最后一步,将这个工厂对象传入视图容器的 createComponent 方法:
      this.const componentRef = container.createComponent(factory)

    通过 createComponent 方法,就可以将这个组件插入到视图中了,并返回这个组件实例。

    有了组件实例,和这个通知组件交互也就不成问题了:

    • 输入属性传值:componentRef.instance.message = "外部传入的警告信息"

    • 绑定输出事件:

      componentRef.instance.closeAlert.subscribe(() => {
        const index = this.container.indexOf(componentRef.hostView);
        this.container.remove(index);
      });
      

    从这个上面的绑定事件也可以看出,视图容器可以容纳任意多个视图,根据视图对象可以查询索引,或者销毁,移除,插入,移动任意视图顺序。

    详解 Angular 动态视图 (一) -- 原生 API

    动态插入模板

    和 createComponent 类似的,通过 createEmbeddedView 就可以插入模板。

    首先先创建一个模板示例,这个模板根据上下文对象声明了一个 “param” 属性:

    <ng-template #templateView let-param="message">
        <section class="template-wrapper">
            <span>来自 ng-template 的动态内容</span>
            <span>{{param}}</span>
        </section>
    </ng-template>
    

    要插入一个带上下文数据的模板,具体分为三步:

    1. 获取模板的引用对象:

      @ViewChild('templateView', { read: TemplateRef }) template!: TemplateRef<any>;
      
    2. 声明上下文对象:templateContext = { message: '来自模板上下文的值' };

    3. 通过视图容器的 createEmbeddedView 方法插入模板:

      const embeddedViewRef = this.container.createEmbeddedView(this.template, this.templateContext);
      

    方法创建一个 EmbeddedViewRef 对象,并将它放入。通过这个对象,视图容器可以查找它的索引,所以也可以和之前组件视图的引用一样,在容器内移动顺序、移除渲染、或是被销毁。

    详解 Angular 动态视图 (一) -- 原生 API

    以指令方式插入

    除了使用视图容器的两个方法来创建和插入视图,Angular 还提供了两个指令来简化工作。

    ngComponentOutlet

    首先,再创建另一个示例组件,和前面的哪个通知组件不同,它没有输入输出属性,但是多了一个需要注入的依赖项,以便演示带依赖注入的组件插入:

    @Component({
        template: `
        <section class="template-wrapper">
            <span>来自另一个动态组件:{{param.message}}</span>
        </section>`
    })
    export class AnotherComponent {
        constructor(public param: ExampleService) { }
    }
    

    使用指令的方式创建组件就简单多了,只需要两步:

    1. 引入这个组件类,并赋值给一个属性:

      import { AnotherComponent } from '../shared/another-component';
      export class ViewContainerExampleComponent implements OnInit, OnDestroy {
           public anotherComponent = AnotherComponent;
      }
      
    2. 在视图中声明即可:

      <ng-container *ngComponentOutlet="anotherComponent"></ng-container>
      

    传入依赖注入器

    正常情况下,这样就把组件插入指定位置了,不过如果动态组件所声明的依赖项,需要由这个组件本身提供呢?

    这里就要再给这个组件传入注入对象:
    <ng-container *ngComponentOutlet="anotherComponent;injector:costumeInjector">

    这个 costumeInjector 可以通过 Injector 类的静态方法创建:

    constructor(
        injector: Injector
    ) {
        this.costumeInjector = Injector.create({ 
            providers: [{ provide: ExampleService, deps: [] }], 
            parent: injector 
        });
    }
    

    这样一来,每个动态创建的组件,都会拥有一个独立的 ExampleService 实例。

    ✨ 视图容器的 createComponent 方法同样可以指定依赖注入器,效果是一样的,前面只是为了简明而省略。

    当然,常见的情况依旧是给 ExampleService 的装饰器声明为全局服务:@Injectable({ providedIn:'root'})

    传入内容映射

    除了可以指定注入器,还可以传入内容映射。

    先给组件做一点小修改,新增一个 <ng-content> 标记,使得这个组件可以接收外部内容映射:

    <section class="template-wrapper">
        <span>来自另一个动态组件:{{param.message}}</span>
        <ng-content></ng-content>
    </section>`
    

    要插入映射的 DOM 内容,只需要额外给指令的表达式再传一个参数:<ng-container *ngComponentOutlet="anotherComponent;content:costumeContent">

    这个 costumeContent 是一个数组,因为组件内可以有多个 ng-content。数组内每项也是一个数组,因为每个 ng-content 位置,可以插入多个 DOM 内容块。

    const spanContent = document.createElement('span');
    const divContent = document.createElement('div');
    spanContent.innerHTML = 'hello, world';
    divContent.innerHTML = '<span>locotor</span>';
    this.costumeContent = [[spanContent, divContent]];
    

    ngTemplateOutlet

    模板的指令只有两个输入属性:模板的引用对象、模板的上下文对象。

    所以要插入一个带上下文数据的模板,具体步骤如下:

    1. 给模板添加引用名:

      <ng-template #templateView let-param="message">
           <!-- 省略内容 -->
      </ng-template>
      
    2. 声明上下文对象:templateContext = { message: '来自模板上下文的值' };

    3. 传入 ngTemplateOutlet 指令中:

      <ng-container *ngTemplateOutlet="templateView; context: templateContext"></ng-container>
      

    对比一下

    前面介绍了两种插入视图的方式,效果都是类似的,但是也有些许不同之处。了解它们的差异,才能根据场景使用合适的实现方式。

    指令和 ViewContainerRef 对象实例的差异主要有两个:

    • 多视图:相比指令的方式来插入视图,通过 ViewContainerRef 的创建方法,在一个视图容器中,可以创建任意多个视图。也因此,通过视图容器还具有对视图的管理能力,例如将某个视图移到容器的顶部,或是销毁某一个视图及其相关数据。

    • 组件实例:通过 createComponent 方法插入组件视图的同时,还可以得到这个组件类的实例。有了它,就可以给组件传参,或是注册它的输出事件。

    除了上述差异,其他的地方都是相同的。对比一下插入组件时,使用指令的方式:

    <ng-container *ngComponentOutlet="componentTypeExpression;
                                      injector: injectorExpression;
                                      content: contentNodesExpression;
                                      ngModuleFactory: moduleFactory;">
    </ng-container>
    

    可以指定要插入组件类,组件的注入器,映射内容,以及模块工厂对象(允许动态加载其他模块)。

    所对应的代码方式插入:

    createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], ngModule?: NgModuleRef<any>): ComponentRef<C>
    

    除了第二个参数是指定插入到容器的顺序序号以外,其他的参数都是一一对应的。

    和组件的情况类似,模板插入除了视图容器支持多个模板以外,可以支持指定插入序号外,和指令的方式完全一样的。都是两个参数,一个是模板引用对象,一个是模板上下文对象。

    总结

    本篇总结了 Angular API 实现动态视图插入的方式。ViewContainerRef 可以支持任意多个视图的插入,对它们进行管理。它插入的组件可以拿到组件实例,能够执行输入输出交互。指令的方式可以便捷的插入视图,但是只能在一个容器内插入一个视图,对组件的输入输出交互支持不足。

    可以看到,通过 Angular 原生 API 已经可以实现动态视图功能,不过如果能结合指令式的便捷,再兼顾组件交互就好了。好在 Material 开发组还提供了一套 Angular CDK(组件开发套件),它的 Portal 模块,封装了原生 API,可以更方便的实现动态视图,我们下篇见!?


    起源地下载网 » 详解 Angular 动态视图 (一) -- 原生 API

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元