wzhudev / blog

:book:
220 stars 14 forks source link

Angular CDK Portal 源码解析 #8

Closed wzhudev closed 3 years ago

wzhudev commented 5 years ago

Portal 用于在任意位置动态渲染模板或组件。

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 这个类其实非常简单,用于挂载和卸载 portal 的几个所做的事情基本都是:检查边界情况,然后调用 PortalOutlet 的对应方法。

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

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

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

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

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

BasePortalOutlet 有以下要点:

cdkPortalOutlet 有以下要点:

在例子里,我们的 portal 是一个 ComponentPortal,这里我们就只分析该情形,即 attachComponentPortal 被调用的情形。

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

其他

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

cdkPortal

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

DomPortalOutlet

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

可以看到 DomPortalOutletcdkPortalOutlet 有以下不同:

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

PortalInjector

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

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

总结

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

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