Angular原生动态加载器与Portal

Angular原生动态加载器与Portal,第1张

文章目录 一、动态加载组件方式一ComponentFactoryResolverComponentFactoryComponentRef 方式二ViewContainerRef 二、动态加载内嵌视图ViewContainerRefEmbeddedViewRefngTemplateOutlet指令实现原理 - createEmbeddedView简写语法扩展语法 三、Angular cdk 之 Portal(动态加载组件、模板 )PortalPortalOutletBasePortalOutletcdkPortalOutlet指令如何得到一个ComponentPortal?如何得到一个TemplatePortal? DomPortalOutlet 类对比 原生api 和 portal

一、动态加载组件 方式一 ComponentFactoryResolver

ComponentFactoryResolver是一个简单的注册表,这个注册表将组件映射到生成的 ComponentFactory 类。

作用:组件的模板不会永远是固定的。应用可能会需要在运行期间去加载一些新的组件。所以我们可以使用 ComponentFactoryResolver 来动态添加组件。

abstract class ComponentFactoryResolver {
  static NULL: ComponentFactoryResolver
​
  // 检索能创建给定类型的组件的工厂对象(实例化给定类型的组件的工厂),并返回该工厂对象ComponentFactory。
  abstract resolveComponentFactory<T>(component: Type<T>): ComponentFactory<T>
}

注意:从 v13 开始,建议使用 ViewContainerRef.createComponent() 创建动态组件。
ViewContainerRef.createComponent() 的第一个参数以前是支持传组件工厂,这个版本之后将弃用。从v13开始,第一个参数支持我们直接传组件类。
(具体看后面ViewContainerRef.createComponent 的使用)

ComponentFactory

ComponentFactory类 可用来动态创建组件的工厂的基类。使用生成的 ComponentFactory.create() 方法创建该类型的一个新组件,返回值是一个组件 ComponentRef。

abstract class ComponentFactory<C> {
  abstract selector: string     // 组件的 HTML 选择器
  abstract componentType: Type<any>   // 工厂将创建的组件的类型
  abstract ngContentSelectors: string[]   // 组件中所有 元素的选择器
  abstract inputs: {...}  // 组件的 inputs
  abstract outputs: {...}  // 组件的 outputs
  abstract create(injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: any, ngModule?: NgModuleRef<any>): ComponentRef<C>   // 创建该工厂对应组件类型的一个新组件
}
ComponentRef

表示由 ComponentFactory 创建的组件。提供对组件实例和相关对象的访问,并提供销毁实例的方法。

abstract class ComponentRef<C> {
  abstract location: ElementRef   // 该组件实例的宿主或锚点元素
  abstract injector: Injector   // 该组件实例的依赖项注入器(dependency injector)。
  abstract instance: C   // 该组件实例
  abstract hostView: ViewRef   // 模板为此组件实例定义的宿主视图 
  abstract changeDetectorRef: ChangeDetectorRef    // 此组件实例的变更检测器
  abstract componentType: Type<any>    // 此组件的类型(由 ComponentFactory 类创建)
  abstract destroy(): void   // 销毁组件实例以及与其关联的所有数据结构
  abstract onDestroy(callback: Function): void   // 一个生命周期钩子,为组件提供其他由开发人员定义的清理功能。
}
方式二 ViewContainerRef

ViewContainerRef 是一个视图容器,通过它,我们可以获取容器中的视图的访问权。当我们在该容器中动态加入组件或内嵌视图,这个容器就是组件或内嵌视图的宿主。

ViewContainerRef给我们提供了一个动态创建组件的方法createComponent(),它会根据我们给定的组件类型来创建一个新组件并插入到视图容器中,最终返回一个组件引用ComponentRef

// Angular v13   根据组件类型创建组件
createComponent<C>(componentType: Type<C>, options?: { index?: number; injector?: Injector; ngModuleRef?: NgModuleRef<unknown>; projectableNodes?: Node[][]; }): ComponentRef<C>
// Angular v13之前的版本:根据组件工厂创建组件,将弃用。
createComponent<C>( componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], ngModuleRef?: NgModuleRef<any>): ComponentRef<C>;
二、动态加载内嵌视图 ViewContainerRef

ViewContainerRef视图容器除了支持我们在视图容器上动态创建组件之外,它还支持我们通过 createEmbeddedView() 方法在视图容器中动态创建内嵌视图。

/**
 * 这个api很有用:实例化一个内嵌视图,并把它插入到该视图容器中
 * 参数1:模板引用TemplateRef(用来定义视图的 HTML 模板)。
 * 参数2:要挂载在内嵌视图上的上下文对象context。
 * 参数3:从 0 开始的索引,表示新视图要插入到当前视图容器的哪个位置。如果没有指定,就把新的视图追加到最后。
 * 
 * 返回值:内嵌视图EmbeddedViewRef。(新创建的视图的 ViewRef 实例)。
 */
abstract createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, index?: number): EmbeddedViewRef<C>
EmbeddedViewRef

EmbeddedViewRef 表示视图容器中的 Angular 视图,也叫内嵌视图。内嵌视图可以从[在模板中定义它的宿主组件之外的]组件中引用,也可以由 TemplateRef 进行独立定义。

我们可以通过在ViewContainerRef视图容器中插入 insert ,移动 move 或删除 remove 内嵌视图EmbeddedViewRef来更改元素的结构。

ngTemplateOutlet指令 实现原理 - createEmbeddedView

ngTemplateOutlet指令的实现原理:

ViewContainerRef.createEmbeddedView(ngTemplateOutlet,ngTemplateOutletContext)

具体描述:

注入ViewContainerRef,获取该指令所在锚点元素的视图容器。使用视图容器的createEmbeddedView()方法并根据传入的模板引用ngTemplateOutlet:TemplateRef 创建内嵌视图。内嵌视图是ViewRef的子类,具有context上下文属性,将传入的ngTemplateOutletContext属性值赋给视图的上下文context。

ngTemplateOutlet指令的实现源码:

import {Directive, EmbeddedViewRef, Input, OnChanges,  SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core';

@Directive({selector: '[ngTemplateOutlet]'})
export class NgTemplateOutlet implements OnChanges {
  private _viewRef: EmbeddedViewRef<any> | null = null;

  // 附加到EmbeddedViewRef的上下文对象。 
  // 这应该是一个对象,对象的键将可用于本地模板 `let` 的绑定声明。在上下文对象中使用键 $implicit 会将其值设置为默认值。
  @Input() public ngTemplateOutletContext: Object | null = null;

  // 定义模板引用和可选的模板上下文对象的字符串。
  @Input() public ngTemplateOutlet: TemplateRef<any> | null = null;

  constructor(
    // 注入视图容器
    private _viewContainerRef: ViewContainerRef
  ) {}

  /**
   * OnChanges 是一个生命周期钩子,当指令的任何一个可绑定属性(ngTemplateOutlet或ngTemplateOutletContext)发生变化时调用。
   * 定义一个 ngOnChanges() 方法来处理这些变更。
   * @param changes
   */
  ngOnChanges(changes: SimpleChanges) {
    if (changes['ngTemplateOutlet']) {
      // 1. 如果传入了一个新的模板引用,则移除当前的视图。
      const viewContainerRef = this._viewContainerRef;

      if (this._viewRef) {
        // 1.1 如果当前有视图,找到该视图在当前视图容器中的索引,并根据索引移除当前视图
        viewContainerRef.remove(viewContainerRef.indexOf(this._viewRef));
      }

      // 1.2 在当前视图容器中添加新的内嵌视图,参数1是模板引用TemplateRef,参数2是要挂载在该内嵌视图的上下文对象。
      this._viewRef = this.ngTemplateOutlet
        ? viewContainerRef.createEmbeddedView(this.ngTemplateOutlet, this.ngTemplateOutletContext)
        : null;
    } else if (
      // 如果传入了一个新的上下文对象 且 当前有视图,则更新当前视图的上下文。
      this._viewRef &&
      changes['ngTemplateOutletContext'] &&
      this.ngTemplateOutletContext
    ) {
      this._viewRef.context = this.ngTemplateOutletContext;
    }
  }
}

ngTemplateOutlet指令的实现是动态创建内嵌视图的最典型的体现。

简写语法

简写语法是使用一个 带 * 前缀的指令 ,上下文对象直接赋值给内嵌视图EmbeddedViewRef的context属性 。

<ng-container *ngTemplateOutlet="templateRefExp; context: contextExp">ng-container>
扩展语法

扩展语法是将指令放在 元素上,将上下文对象赋给ngTemplateOutletContext属性, 指令内部逻辑会通过 this._viewRef.context = this.ngTemplateOutletContext; 来将上下文对象(键值对)赋值给内嵌视图的context属性。

<ng-template [ngTemplateOutlet]="templateRefExp" [ngTemplateOutletContext]="contextExp">ng-template>

上下文对象的键名可以在局部模板中使用 let 声明进行绑定。上下文对象中使用 $implicit 为键名时,则将把它作为默认值。

三、Angular cdk 之 Portal(动态加载组件、模板 )

portals 包提供了一个灵活的布局体系,可以把动态内容渲染到应用中。
核心:允许我们将想要展示的内容( Portal )动态渲染到页面上的空白插槽( PortalOutlet )中。

Portal

Portal是我们要动态渲染到页面的内容。它可以是:

组件Component (ComponentPortal )模板TemplateRef (TemplatePortal)DOM 元素 (DomPortal)

Portal抽象类:


PortalOutlet

相当于页面上的一个空白插槽,我们可以将Component、TemplateRef 或 DOM 元素这些东西放在PortalOutlet中渲染出来。该接口有以下4个方法:

export interface PortalOutlet {
  // 将一个portal(组件或模板)附着到当前的插槽上
  attach(portal: Portal<any>): any;

  // 将附着的portal从插槽上卸下来
  detach(): any;

  // 在outlet被销毁前进行清理
  dispose(): void;

  // 是否有portal附着在当前的outlet上
  hasAttached(): boolean;
}

BasePortalOutlet

为了让大家使用起来更方便,cdk又封装了一个基类BasePortalOutlet。该基类实现了PortalOutlet的attach方法,封装了attach的一些行为,并向外暴露两个函数 attachComponentPortalattachTemplatePortal

// 重载(多个同名方法,参数和返回值不同,目的:为了能根据传入的不同类型来返回对应类型的返回值)
attach<T>(portal: ComponentPortal<T>): ComponentRef<T>;
attach<T>(portal: TemplatePortal<T>): EmbeddedViewRef<T>;
attach(portal: any): any;


abstract attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T>;
abstract attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C>;

attachComponentPortal 底层实现是: ViewContainerRef.createComponent()

attachTemplatePortal 底层实现是: ViewContainerRef.createEmbeddedView()

cdkPortalOutlet指令

关键点1:cdkPortalOutlet指令继承自 BasePortalOutlet。

关键点2:cdkPortalOutlet指令接收的参数可以是ComponentPortal,也可以是TemplatePortal。

关键点3:我们将Portal传给cdkPortalOutlet指令后,指令内部会判断传进来的是个组件还是模板,然后调 用angular原生api去动态创建组件或内嵌视图,然后渲染到cdkPortalOutlet指令所在的锚点上。

用法如下:

<ng-template [cdkPortalOutlet]="greeting">ng-template>




如何得到一个ComponentPortal?
// 最简单:
 const componentPortal = new ComponentPortal(component); 

// 最具体:
const componentPortal = new ComponentPortal(component, viewContainerRef, injector, componentFactoryResolver);

/**
 * 参数1:必填,传一个组件。
 * 参数2:选填,视图容器,决定了该组件将附着到组件树的哪个位置。
 * 参数3:选填,用于实例化组件的注入器。
 * 参数4:选填,默认会使用outlet的componentFactoryResolver。
 */
如何得到一个TemplatePortal?
// 最简单:
const templatePortal = new TemplatePortal(templateRef, viewContainerRef); 

// 最具体:
const templatePortal = new TemplatePortal(templateRef, viewContainerRef, context, injector);

/**
 * 参数1:必填,用于实例化宿主中内嵌视图的模板。
 * 参数2:必填,对视图容器的引用,模板将被插入的哪个视图容器。
 * 参数3:可选,内嵌视图的上下文数据。
 * 参数4:可选,内嵌视图的依赖注入器。
 */
DomPortalOutlet 类

DomPortalOutlet继承自BasePortalOutlet基类。

为什么会DomPortalOutlet有这个类? ??

使用场景:不能使用模板的cdkPortalOutlet指令的场景下,比如overlay它是一个浮层,它是脱离页面浮在上方的,我们不可能通过在ng-template中去使用cdkPortalOutlet指令来让浮层渲染在页面中。这时候就需要使用到 DomPortalOutlet 类了。

overlay的create方法创建OverlayRef实例时就使用了 DomPortalOutlet 来创建用于渲染内容的插槽。

对比 原生api 和 portal

使用原生angular api动态加载组件或模板,需要我们自己判断是加载组件还是加载模板来手动调用不同的方法。
Portal相当于是从使用更方便的角度去为我们封装了前者的复杂判断和实现,我们直接将组件或模板作为portal传给portalOutlet去让它自己判断并实现即可,它会判断并使用相应的方法去动态创建内容并附着到目标锚点上。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/945087.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-18
下一篇 2022-05-18

发表评论

登录后才能评论

评论列表(0条)

保存