Angular CDK Portal 源码解析

Portal (opens in a new tab) 用于在任意位置动态渲染模板或组件。

Portal 指的是需要动态创建的内容(模板或者组件),而 PortalOutlet 指的是渲染这些内容的位置。

目录结构

portal
├── BUILD.bazel
├── dom-portal-outlet.ts // DomPortalOutlet
├── index.ts
├── portal-directives.ts // 包含所有的指令,使得你能够以声明式的使用方式来使用 Portal
├── portal-errors.ts // 包含所有的错误信息
├── portal-injector.ts // 你可以临时创建一个 Injector  Portal,从而干预依赖注入
├── portal.md
├── portal.spec.ts
├── portal.ts // 定义了核心部分的几个类
├── public-api.ts
└── tsconfig-build.json

最重要的文件有以下三个:

  1. portal.ts,这个文件定义了抽象类 PortalBasePortalOutlet,还定义了类 ComponentPortalTemplatePortal
  2. portal-directive.ts,这个文件里定义了指令 CdkPortalCdkPortalOutlet,让我们可以以声明式的方式使用 portal,后者还定义了动态创建内容的机制。
  3. dom-portal-outlet.ts,这个文件里定义了 DomPortalOutlet,使得 portal 可以被渲染到 Angular 的组件树之外(这一点被 overlay 模块所使用,之后会 cover 到这部分内容)。

核心机制

以 README 中的示例来讲解这部分代码是如何工作的:

this.userSettingsPortal = new ComponentPortal(UserSettingsComponent)
<!-- Attaches the `userSettingsPortal` from the previous example. -->
<ng-template [cdkPortalOutlet]="userSettingsPortal"></ng-template>

示例中的代码首先创建了一个 ComponentPortal,那我们就先来看 ComponentPoral 和其父类 Portal

Portal (opens in a new tab) 这个类其实非常简单,用于挂载和卸载 portal 的几个所做的事情基本都是:检查边界情况,然后调用 PortalOutlet 的对应方法。

ComponentPortal (opens in a new tab) 这个类也非常简单,它仅仅是对动态创建组件所需要的数据结构的一个封装。这些数据结构包括:

后面三个参数在构造一个 ComponentPortal 的时候都是可选的,注意这一点,之后在讲解动态渲染过程中就会了解到为啥是可选的。

示例中的代码到这里,就会给 cdkPortalOutlet 赋值这个新创建的 ComponentPortal

<ng-template [cdkPortalOutlet]="userSettingsPortal"></ng-template>

我们再来看 cdkPortalOutlet 和其父类 BasePortalOutlet 的代码。

BasePortalOutlet (opens in a new tab) 有以下要点:

cdkPortalOutlet (opens in a new tab) 有以下要点:

在例子里,我们的 portal 是一个 ComponentPortal,这里我们就只分析该情形,即 attachComponentPortal 被调用 (opens in a new tab)的情形。

到这里,就是 Portal 机制的主要运行过程了。

其他

接下来我们 cover 一些之前没有 cover 到的要点。

cdkPortal

cdkPortal (opens in a new tab) 允许使用者以声明式的方式创建一个 TemplatePortal,它的代码非常简单,仅仅是把 TemplatePortal 变成了一个结构型指令。

DomPortalOutlet

我们看到 cdkPortalOutlet 是以声明式方式使用的,这意味着它必须在 Angular 的组件树当中,如果我们想把 portal 挂载到组件树之外的位置,就需要用 DomPortalOutlet

可以看到 DomPortalOutletcdkPortalOutlet 有以下不同:

以上不同的根源都是挂载到组件树之外的 portal 可能会没有 ViewContaienerRef

PortalInjector

在创建 ComponentPortal 的时候我们可以传入一个 Injector (opens in a new tab),而 PortalInjector (opens in a new tab) 允许我们对这个 Injector 作出干预,增加新的 provider。

可以看到它所做的全部事情,其实就是在返回依赖项的时候,检查用户而外提供的 provider 里有没有符合依赖注入令牌的 (opens in a new tab)

总结

我们都知道通过 Angular 的 ViewContrainerRef 提供的 createEmbedeViewcreateComponent 方法就可以动态创建界面内容,为什么还需要 CDK 提供的 Portal 呢?通过上文的分析,我们可以得出使用 Portal 的一些好处:

Portal 被 Angular CDK 的其他很多模块所采用。

, CC BY-NC 4.0 © Wenzhao.