添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
import { TaskDetailComponent } from '../task/task-detail/task-detail.component'; @Component({ selector: 'app-portals-entry', templateUrl: './portals-entry.component.html', styleUrls: ['./portals-entry.component.scss'], providers: [ export class PortalsEntryComponent implements OnInit { @ViewChild('virtualContainer', { read: ViewContainerRef }) virtualContainer: ViewContainerRef; constructor( private dynamicComponentService: DynamicComponentService, private componentFactoryResolver: ComponentFactoryResolver, private injector: Injector, ) { } ngOnInit() { openTask() { const task = new TaskEntity(); task.id = '1000'; task.name = '写一篇关于Portals的文章'; const componentFactory = this.componentFactoryResolver.resolveComponentFactory(TaskDetailComponent); const componentRef = this.virtualContainer.createComponent<TaskDetailComponent>( componentFactory, null, this.virtualContainer.injector (componentRef.instance as TaskDetailComponent).task = task; // 传递参数
  1. openTask()方法绑定到模板中按钮的单击事件
  2. 导入要动态创建的组件TaskDetailComponent
  3. constructor注入injector、componentFactoryResolver 动态创建组件需要的对象,只有在组件上下文中才可以拿到这些实例对象
  4. 使用api创建组件,现根据组件类型创建一个ComponentFactory对象,然后调用viewContainer的createComponent创建组件
  5. 使用componentRef.instance获取创建的组件实例,这里用来设置组件的task属性值

ViewContainerRef除了createComponent方法外还有一个createEmbeddedView方法,用于创建模板

@ViewChild('customTemplate')
customTemplate: TemplateRef<any>;
this.virtualContainer.createEmbeddedView(this.customTemplate, { name: 'pubuzhixing' }); 

createEmbeddedView方法的第二个参数,用于指定模板的上下文参数,看下模板定义及如何使用参数

<ng-template #customTemplate let-name="name">
  <p>自定义模板,传入参数name:{{name}}</p>
</ng-template> 

此外还可以通过ngTemplateOutlet直接插入内嵌视图模板,通过ngTemplateOutletContext指定模板的上下文参数

<ng-container [ngTemplateOutlet]="customTemplate" [ngTemplateOutletContext]="{ name:'pubuzhixing' }"></ng-container>

分析下Angular动态创建组件/内嵌视图的API,动态创建组件首先需要一个被创建的组件定义或模板声明,另外需要Angular上下文的环境来提供这个组件渲染在那里以及这个组件的依赖从那获取,viewContainerRef是动态组件的插入位置并且提供组件的逻辑范围,此外还需要单独传入依赖注入器injector,示例直接使用逻辑容器的injector,是不是很好理解。
示例仓储:https://github.com/pubuzhixing8/angular-cdk-demo

CDK Portal 官方文档介绍

这里先对Portal相关的内容做一个简单的说明,后面会有两个使用示例,本来这块内容准备放到最后的,最终还是决定放在前面,可以先对Portals有一个简单的了解,如果其中有翻译不准确请见谅。
地址:https://material.angular.io/cdk/portal/overview

——– 文档开始
portals 提供渲染动态内容到应用的可伸缩的实现,其实就是封装了Angular动态创建组件的过程

Portals

这个Portal指是能动态渲染一个指定位置的 UI块 到页面中的一个 open slot  。
 UI块 指需要被动态渲染的内容,可以是一个组件或者是一个模板,而 open slot 是一个叫做PortalOutlet的开放的占位区域。
Portals和PortalOutlets是其它概念中的低级的构造块,像overlays就是在它基础上构建的

 Portal<T> 包括动态组件的抽象类,可以是TemplatePortal(模板)或者ComponentPortal(组件)

方法描述attach(PortalOutlet): T把当前Portal附加到宿主上detach(): void把Portal从宿主上拆离isAttached: boolean当前Portal是否已经附加到宿主上

 PortalOutlet 动态组件的宿主

方法描述attach(Portal): any附加指定Portaldetach(): any拆离当前附加Portaldispose(): void永久释放宿主资源hasAttached: boolean当前是否已经装在Portal#### 代码片段说明CdkPortal“`

The content of this template is captured by the portal.<p *cdkPortal>The content of this template is captured by the portal.

“`可以通过ViewChild、ViewChildren获取到该Portal,类型应该是CdkPortal,如下所示:“`// 模板中的Portal@ViewChild(CdkPortal) templateCDKPortal: TemplatePortal;“`ComponentPortal组件类型的Portal,需要当前组件在NgModule的entryComponents中配置才能动态创建该组件。“`this.userSettingsPortal = new ComponentPortal(UserSettingsComponent);“`CdkPortalOutlet使用指令可以把portal outlet添加到一个ng-template,cdkPortalOutlet把当前元素指定为PortalOutlet,下面代码把userSettingsPortal绑到此portal-outlet上“`<ng-template [cdkPortalOutlet]=”userSettingsPortal”>“`—– 文档完毕

** Portals使用示例**

这里首先使用新的api完成和最上面示例一样的需求,在同样的位置动态渲染TaskDetailComponent组件。

名列前茅步

同样是设置一个宿主元素用于渲染动态组件,可以使用指令cdkPortalOutlet挂载一个PortalOutlet在这个ng-container元素上

<div class="portals-outlet">
   <ng-container #virtualContainer cdkPortalOutlet>
   </ng-container>

与 使用Angular API动态创建组件 一节使用同一个逻辑元素作为宿主,只不过这里的获取容器的类型是CdkPortalOutlet,代码如下

@ViewChild('virtualContainer', { read: CdkPortalOutlet })
virtualPotalOutlet: CdkPortalOutlet;

创建一个ComponentPortal类型的Portal,并且将它附加上面获取的宿主virtualPotalOutlet上,代码如下

  portalOpenTask() {
    this.virtualPotalOutlet.detach();
    const taskDetailCompoentPortal = new ComponentPortal<TaskDetailComponent>(
      TaskDetailComponent
    const ref = this.virtualPotalOutlet.attach(taskDetailCompoentPortal);
    // 此处同样可以 通过ref.instance传递task参数

这里是使用ComponentPortal的示例实现动态创建组件,Portal还有一个子类TemplatePortal是针对模板实现的,上节 CDK Portal 官方文档介绍 中有介绍,这里就不在赘述了。总之使用Portals可以很大程度上简化代码逻辑。
示例仓储:https://github.com/pubuzhixing8/angular-cdk-demo

Portals 源码分析

上面只是使用Portal的最简单用法,下面讨论下它的源码实现,以便更好的理解

ComponentPortal

首先我们先看一下ComponentPortal类的创建,上面的例子只是指定了一个组件类型作为参数,其实它还有别的参数可以配置,先看下ComponentPortal的构造函数定义

export class ComponentPortal<T> extends Portal<ComponentRef<T>> {   
  constructor(
      component: ComponentType<T>,
      viewContainerRef?: ViewContainerRef | null,
      injector?: Injector | null,
      componentFactoryResolver?: ComponentFactoryResolver | null) {
    super();
    this.component = component;
    this.viewContainerRef = viewContainerRef;
    this.injector = injector;
    this.componentFactoryResolver = componentFactoryResolver;

ComponentPortal构造函数的另外两个参数 viewContainerRef 和  injector
viewContainerRef 参数非必填默认附到PortalOutlet上,如果传入viewContainerRef参数,那么ComponentPortal就会附到该viewContaierRef上,而不是当前PortalOutlet所在的元素上。
injector 参数非必填,默认使用PortalOutlet所在的逻辑容器的injector,如果传入injector,那么动态创建的组件就使用传入的injector作为注入器。

** BasePortalOutlet**

BasePortalOutlet提供了附加ComponentPortal和TemplatePortal的部分实现,我们看下attach方法的部分代码(仅仅展示部分逻辑)

  /** Attaches a portal. */
  attach(portal: Portal<any>): any {
    if (!portal) {
      throwNullPortalError();
    if (portal instanceof ComponentPortal) {
      this._attachedPortal = portal;
      return this.attachComponentPortal(portal);
    } else if (portal instanceof TemplatePortal) {
      this._attachedPortal = portal;
      return this.attachTemplatePortal(portal);
    throwUnknownPortalTypeError();

attach处理前先根据Portal的类型是确实是组件还是模板,然后再进行相应的处理,其实最终还是调用了ViewContainerRef的createComponent或者createEmbeddedView方法,对这块感兴趣看查看源代码文件portal-directives.ts

** DomPortalOutlet**

DomPortalOutlet可以把一个Portal插入到一个Angular应用上下文之外的DOM中,想想我们前面的例子,无论自己实现还是使用CdkPortalOutlet都是把一个模板或者组件插入到一个Angular上下文中的宿主ViewContainerRef中,而DomPortalOutlet就是 脱离Angular上下文 的宿主,可以把Portal渲染到任意dom中,我们常常有这种需求,比如弹出的模态框、Select浮层。
在cdk中Overlay用到了DomPortalOutlet,然后material ui的MatMenu也用到了DomPortalOutlet,MatMenu比较容易理解,简单看下它是如何创建和使用的DomPortalOutle(查看全部

if (!this._outlet) {
    this._outlet = new DomPortalOutlet(this._document.createElement('div'),
    this._componentFactoryResolver, this._appRef, this._injector);
const element: HTMLElement = this._template.elementRef.nativeElement;
element.parentNode!.insertBefore(this._outlet.outletElement, element);
this._portal.attach(this._outlet, context);

上面的代码先创建了DomPortalOutlet类型的对象_outlet,DomPortalOutlet是一个DOM宿主它不在Angular的任何一个ViewContainerRef中,现在看下它的四个构造函数参数

参数名类型说明outletElementElement创建的document元素_componentFactoryResolverComponentFactoryResolver刚开始一直不理解这个实例对象是干什么的,后来查了资料,它大概的作用是对要创建的组件或者模板进行编译_appRefApplicationRef当前Angular应用的一个关联对象_defaultInjectorInjector注入器对象

说明:这节讲的 脱离Angular上下文 是不太准确定,任何模板或者组件都不能脱离Angular的运行环境,这里应该是脱离了实际渲染的Component Tree,单独渲染到指定dom中。

为ComponentPortal传入PortalInjector对象,PortalInjector实例对象配置一个其它业务组件的injector并且配置tokens,下面简单说明下逻辑结构,有兴趣的可看完整示例

业务组件TaskListComponent

文件task-list.component.ts

@Component({,
  selector: 'app-task-list',
  templateUrl: './task-list.component.html',
  styleUrls: ['./task-list.component.scss'],
  providers: [TaskListService]
export class TaskListComponent implements OnInit {
  constructor(public taskListService: TaskListService) {}

组件级提供商配置了TaskListService

定义TaskListService

用于获取任务列表数据,并保存在属性tasks中

TaskListComponent模板

在模板中直接绑定taskListService.tasks属性数据

修改父组件PortalsEntryComponent

因为PortalOutlet是在父组件中,所以单击任务列表创建动态组件的逻辑是从父组件响应的
portals-entry.component.ts

   @ViewChild('taskListContainer', { read: TaskListComponent })
  taskListComponent: TaskListComponent; 
  ngOnInit() {
    this.taskListComponent.openTask = task => {
      this.portalCreatTaskModel(task);
portalCreatTaskModel(task: TaskEntity) {
    this.virtualPotalOutlet.detach();
    const customerTokens = new WeakMap();
    customerTokens.set(TaskEntity, task);
    const portalInjector = new PortalInjector(
      this.taskListViewContainerRef.injector,
      customerTokens
    const taskModelCompoentPortal = new ComponentPortal<TaskModelComponent>(
      TaskModelComponent,
      null,
      portalInjector
    this.virtualPotalOutlet.attach(taskModelCompoentPortal);

给ComponentPortal的构造函数传递了PortalInjector类型的参数portalInjector,PortalInjector继承自Injector

PortalInjector构造函数的两个参数

  1. 名列前茅个参数是提供一个基础的注入器injector,这里使用了taskListViewContainerRef.injector,taskListViewContainerRef就是业务TaskListComponent组件的viewContainerRef
@ViewChild('taskListContainer', { read: ViewContainerRef })
taskListViewContainerRef: ViewContainerRef; 

也就是新的组件的注入器来自于TaskListComponent

  1. 第二个参数是提供一个tokens,类型是WeakMap,其实就是key/value的键值对,只不过它的key只能是引用类型的对象,这里把类型TaskEntity作为key,当前选中的实例对象作为value,就可以实现对象的传入,使用set方法customerTokens.set(TaskEntity, task);

新的任务详情组件TaskModelComponent

task-model.component.ts

  constructor(
    public task: TaskEntity,
    private taskListService: TaskListService

没错,是通过注入器注入的方式获取TaskEntity实例和服务TaskListService的实例taskListService。

** 小结**

这个例子相对复杂,只是想说明可以给动态创建的组件传入特定的injector。

想写Portals的使用主要是看了我们组件库中模态框ThyDialog的实现,觉得这些用法比较巧妙,所以想分享出来。
示例仓储:https://github.com/pubuzhixing8/angular-cdk-demo
组件库仓储:https://github.com/worktile/ngx-tethys

ViewContainerRef

angula.cn解释:表示可以将一个或多个视图附着到组件中的容器,可以包含宿主视图(当用 createComponent() 方法实例化组件时创建)和内嵌视图(当用 createEmbeddedView() 方法实例化 TemplateRef 时创建)。
我这里的理解ViewContainerRef是Angular中的一个逻辑单元,简单理解它与组件或者页面中的html元素一一对应只是逻辑形态不同,它也有层级只是层级与组件树的层级不是一一对应,这点个人感觉有些难理解,就拿Portals里面ComponentPortal的实现来说,构造函数里面可以传入一个viewContainerRef,代码片段

* A `ComponentPortal` is a portal that instantiates some Component upon attachment. export class ComponentPortal<T> extends Portal<ComponentRef<T>> { * [Optional] Where the attached component should live in Angular's *logical* component tree. * 可选参数 关联的组件应该寄宿的逻辑组件树的位置 * This is different from where the component *renders*, which is determined by the PortalOutlet. * 这跟组件真正渲染的位置是不同的,真正的位置由PortalOutlet决定 * The origin is necessary when the host is outside of the Angular application context. * 当宿主是在Angular上下文环境之外这个参数是必填项 viewContainerRef?: ViewContainerRef | null; constructor( component: ComponentType<T>, viewContainerRef?: ViewContainerRef | null, injector?: Injector | null, componentFactoryResolver?: ComponentFactoryResolver | null) { // ...

对其中viewContainerRef的注释进行了简单的翻译,但还是不知道它是怎么实现逻辑组件树与真实渲染组件树设置不同层级,经过自己的尝试当设置viewContainerRef后,组件就渲染在了传入的viewContainerRef里面。
属性    element    和  injector
* *element * 的类型是ElementRef,用来标识本容器在父容器中的位置与html中的元素一一对应
** *injector ** 的类型是Injector,它是容器的一个依赖注入器对象,我们在组件的constructor中注入的服务以及获取关联的对象都要通过它来查找,在ViewContainer的逻辑树中注入器对象有一个 注入器冒泡 机制,当一个组件申请获得一个依赖时,Angular 先尝试用该组件容器自己的注入器来满足它,在该组件的容器中找不到实例并且也没有配置注入器提供商(providers),他就会在把这个申请转给它父组件的注入器来处理。所以在动态创建组件的时候可以单独配置这个injector可以子组件传递数据、共享实例对象。

WeakMap

最初因为不了解WeakMap而对这个实现疑惑不解,查了WeakMap的相关资料

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键名必须是对象,而值可以是任意的。
键名是对象的弱引用,当对象被回收后,WeakMap自动移除对应的键值对,WeakMap结构有助于防止内存泄漏。
可以与Map对比理解,Map中key可以是各种类型,而WeakMap必须是对象。
这样WeakMap就可以用来在不修改原引用类型对象的基础上,而扩充该对象的属性值,并且不影响引用类型对象的垃圾回收,随该对象的消失,扩充属性随之消失。

作者:Worktile工程师 杨振兴

文章标题:Angular动态创建组件之Portals,发布者:刘佳,转载请注明出处:https://worktile.com/kb/p/6517

(0)
微信扫一扫 支付宝扫一扫