Angular cdk 之 Overlay 源码分析

1,191 阅读11分钟

一、Overlay简介

Overlay 包提供了一种在屏幕上打开浮动面板的方法。比如popover、dialog等组件的实现就用到了Overlay包。

二、Overlay 源码阅读

2.1 OverlayConfig 浮层配置

创建overlay时使用的初始化配置。

// 创建overlay时使用的初始化配置
export class OverlayConfig {
  // 位置策略
  positionStrategy?: PositionStrategy;
  // 滚动策略
  scrollStrategy?: ScrollStrategy = new NoopScrollStrategy();
  // 给 overlay pane 添加自定义样式
  panelClass?: string | string[] = '';
  // overlay是否需要背景幕布
  hasBackdrop?: boolean = false;
  // 自定义背景幕布的样式
  backdropClass?: string | string[] = 'cdk-overlay-dark-backdrop';
  // overlay panel 的宽、高、最大最小宽高,值为number时单位是px
  width?: number | string;
  height?: number | string;
  minWidth?: number | string;
  minHeight?: number | string;
  maxWidth?: number | string;
  maxHeight?: number | string;
  // overlay panel 的文本方向。 Direction = 'ltr' | 'rtl'
  direction?: Direction | Directionality;
  // 当用户在历史中后退或前进时是否需要处理overlay。请注意,这通常不包括点击链接(除非用户正在使用 `HashLocationStrategy`)
  disposeOnNavigation?: boolean = false;

2.2 OverlayContainer 浮层容器

OverlayContainer 提供了一个容器元素的引用,浮层中的每个元素都渲染在其中。默认情况下,浮层容器会直接附加到文档的 body 中,这样就不会被带有 overflow: hidden 的父元素裁剪掉了。

方法说明
getContainerElement(): HTMLElement返回overlay容器元素 。如果已有容器元素则直接返回,如果没有则:创建一个样式名为cdk-overlay-container的div元素并追加到document的body末尾,用于包裹全部的浮层元素。
ngOnDestroy()销毁overlay容器元素

部分源码: 在这里插入图片描述

在开源组件库ngx-tethys的dialog中的体现: 在这里插入图片描述

2.3 Overlay 服务

2.3.1 Overlay服务对外提供了两个方法

方法说明
create(config?: OverlayConfig): OverlayRef{}创建一个overlay浮层。
position(): OverlayPositionBuilder{}获取可用于通过 fluent API 构建和配置位置策略的位置构建器OverlayPositionBuilder

2.3.2 创建浮层

调用 overlay.create() 将返回一个 OverlayRef 实例。该实例用于管理那个特定浮层。 OverlayRef 是一个 PortalOutlet。一旦创建它,就可以为它附加一个 Portal 来添加内容。

const overlayRef = overlay.create();
const userProfilePortal = new ComponentPortal(UserProfile);
overlayRef.attach(userProfilePortal);

2.3.3 create方法具体做了什么?

在这里插入图片描述

第一步:获取或创建overlay容器元素(overlay container element) 获取已有的overlay容器元素,如果没有则:创建一个带有“cdk-overlay-container”样式类的 div作为overlay容器元素并把它追加到document body上。 在这里插入图片描述

第二步:创建overlay宿主元素(overlay host element) 创建一个宿主元素div (that wraps around an overlay and can be used for advanced positioning),并把它追加到容器元素上。 在这里插入图片描述

第三步:创建overlay面板元素(overlay pane element) 创建一个面板元素div,设置它的id为cdk-overlay-xx,同时设置它的样式cdk-overlay-pane,并把它追加到宿主元素上。 在这里插入图片描述

第四步:创建一个可以用于放Portal内容的插槽 DomPortalOutlet,Portal最终会被挂载到overlay pane元素上。

Portal 可以是 Component、TemplateRef 或 DOM 元素,可以被动态渲染到页面上的空白插槽(PortalOutlet)中。

  // 创建一个可以用于放Portal内容的插槽 DomPortalOutlet
  private _createPortalOutlet(pane: HTMLElement): DomPortalOutlet {
    // 我们需要解析ApplicationRef以允许人们在app初始化期间使用基于overlay的提供者。
    if (!this._appRef) {
      this._appRef = this._injector.get<ApplicationRef>(ApplicationRef);
    }

    return new DomPortalOutlet(
      pane, // outletElement is overlay pane,portal将被挂载到overlay pane元素上。
      this._componentFactoryResolver,
      this._appRef,
      this._injector,
      this._document,
    );
  }

第五步:根据传入的overlayConfig来初始化overlay的配置。 第六步:最终创建并返回一个Overlay实例 OverlayRef。

2.4 OverlayRef

OverlayRef 是一个 PortalOutlet。可以通过attach方法为它附加一个 Portal 来添加内容。

export class OverlayRef implements PortalOutlet, OverlayReference {}

2.4.1 OverlayReference

OverlayReference是Overlay的基础接口。用于避免 OverlayRef、PositionStrategy 和 ScrollStrategy 以及 OverlayConfig 之间的循环类型引用。

export interface OverlayReference {
  // 将带有内容的portal附加到这个overlay浮层上
  attach: (portal: Portal<any>) => any;
  // 从portal上解除overlay的挂载
  detach: () => any;
  // 从DOM上清理掉overlay
  dispose: () => void;
  // 返回overlay的html元素(pane element)
  overlayElement: HTMLElement;
  // 返回overlay的宿主元素(host element,that wraps around an overlay)
  hostElement: HTMLElement;
  // 返回overlay的幕布元素
  backdropElement: HTMLElement | null;
  // 获取当前overlay的不可变配置OverlayConfig
  getConfig: () => any;
  // overlay上是否附着着内容
  hasAttached: () => boolean;
  // 更新overlay panel的大小属性 (width,height,minWidth,minHeight,maxWidth,maxHeight)
  updateSize: (config: any) => void;
  // 根据位置策略更新overlay的位置(具体实现:OverlayRef._positionStrategy.apply())
  updatePosition: () => void;
  // 返回overlay panel上内容的布局方向 (ltr or rtl)
  getDirection: () => Direction;
  // 设置overlay panel上文本的方向 (设置dir属性为ltr或rtl)
  setDirection: (dir: Direction | Directionality) => void;
  // 获取overlay幕布被点击时发出的可观察对象
  backdropClick: () => Observable<MouseEvent>;
  // 获取overlay被挂载时发出的可观察对象
  attachments: () => Observable<void>;
  // 获取overlay被取消挂载时发出的可观察对象
  detachments: () => Observable<void>;
  // 获取overlay的keydown事件发出的可观察对象
  keydownEvents: () => Observable<KeyboardEvent>;
  // 获取overlay之外的pointer事件发出的可观察对象
  outsidePointerEvents: () => Observable<MouseEvent>;
  // 给overlay panel添加自定义样式
  addPanelClass: (classes: string | string[]) => void;
  // 移除overlay panel上的样式
  removePanelClass: (classes: string | string[]) => void;
  // overlay外部的鼠标事件流
  readonly _outsidePointerEvents: Subject<MouseEvent>;
  // overlay上的键盘事件流
  readonly _keydownEvents: Subject<KeyboardEvent>;
}

2.4.2 attach 方法具体做了哪些事情?

从以下源码可知:OverlayRef的attach方法帮助我们将portal提供的内容附加到overlay浮层上。其实现过程做了很多事情,具体看以下代码及注释。

  attach(portal: Portal<any>): any {
    // 1. 将overlay宿主元素插入到DOM中,否则动画模块将在重复附加时跳过动画。
    if (!this._host.parentElement && this._previousHostParent) {
      this._previousHostParent.appendChild(this._host);
    }

    // 2. 将portal附着到PortalOutlet上
    const attachResult = this._portalOutlet.attach(portal);

    // 3. 将配置的位置策略附加到当前的overlay上
    if (this._positionStrategy) {
      this._positionStrategy.attach(this);
    }

    /**
     * 4. 更新元素的堆叠顺序,必要时将其移至顶部。
     * (特别是在一个overlay被detach,而另一个应该在其后面的覆盖层被破坏时更需要此步骤。
     * 否则下次打开它们时,堆叠会出错,因为被detach的元素的pane仍然在其原始 DOM 位置。)
     */
    this._updateStackingOrder();

    // 5. 基于传入的overlayConfig更新overlay pane的样式(width、height、minWidth、minHeight、maxWidth、maxHeight)
    this._updateElementSize();

    // 6. 通过修改dir属性值,更新overlay panel的文本方向
    this._updateElementDirection();

    // 7. 启用OverlayConfig配置的滚动策略
    if (this._scrollStrategy) {
      this._scrollStrategy.enable();
    }

    // 8. 在zone稳定后更新位置,以便overlay在定位之前完全渲染,因为位置可能取决于渲染内容的大小。
    this._ngZone.onStable.pipe(take(1)).subscribe(() => {
      // 在zone稳定之前,overlay有可能已经分离,所以在确保overlay挂载的情况下去更新位置策略。
      if (this.hasAttached()) {
        this.updatePosition();
      }
    });

    // 9. 为overlay pane元素启用pointer事件
    this._togglePointerEvents(true);

    // 10. 根据OverlayConfig创建并配置overlay的幕布元素
    if (this._config.hasBackdrop) {
      this._attachBackdrop();
    }

    // 11. 根据OverlayConfig设置overlay pane的样式
    if (this._config.panelClass) {
      this._toggleClasses(this._pane, this._config.panelClass, true);
    }

    // 12. 只有所有的其它配置都完成了,才会发出attachments事件
    this._attachments.next();

    // 13. 通过keyboard dispatcher跟踪这个overlay
    this._keyboardDispatcher.add(this);

    // 14. 如果OverlayConfig的disposeOnNavigation为true,则当用户在历史中后退或前进时从DOM中清除调当前这个overlay
    if (this._config.disposeOnNavigation) {
      this._locationChanges = this._location.subscribe(() => this.dispose());
    }

    // 15. 通过overlay外部的点击事件跟踪这个overlay
    this._outsideClickDispatcher.add(this);

    return attachResult;
  }

其中第10步:根据OverlayConfig创建并配置overlay的幕布元素具体做了以下事情。 在这里插入图片描述

2.5 OverlayPositionBuilder (overlay定位策略构建器)

overlay位置策略构建器,它提供了两个方法:

方法说明
global(): GlobalPositionStrategy {}创建全局定位策略
flexibleConnectedTo(origin: FlexibleConnectedPositionStrategyOrigin ): FlexibleConnectedPositionStrategy根据用于定位overlay的源点来创建一个灵活的定位策略

在这里插入图片描述

2.5.1 PositionStrategy

positionStrategy 配置项决定了浮层在屏幕上的定位方式。PositionStrategy 接口支持我们创建自定义位置策略。

export interface PositionStrategy {
  // 将位置策略附加到当前的overlay
  attach(overlayRef: OverlayReference): void;

  // 更新overlay元素的位置
  apply(): void;

  // 解除定位策略
  detach?(): void;

  // 必要时清除掉位置策略对Dom的任何修改
  dispose(): void;
}

2.5.2 GlobalPositionStrategy 全局定位策略

为浮层添加相对于浏览器视口的显示位置,位置与别的元素无关。这通常用于模态对话框和应用级通知。 通过OverlayPositionBuilder.global()方法创建一个全局的定位策略 GlobalPositionStrategy。全局位置策略给我们提供了以下方法:

方法说明
attach(overlayRef: OverlayReference): void用于更新overlay pane元素的宽高,同时给overlay host元素添加上样式cdk-global-overlay-wrapper
top (topOffset: string = ''): this设置overlay的top定位
left (leftOffset: string = ''): this设置overlay的left定位
bottom (bottomOffset: string = ''): this设置overlay的bottom定位
right (rightOffset: string = ''): this设置overlay的right定位
start (offset: string = ''): this设置overlay相对于浏览器视口的起点,参数是相对于浏览器左边缘(overlay布局为LTR时)或右边缘(overlay布局为RTL时)的偏移量
end (offset: string = ''): this设置overlay相对于浏览器视口的终点,参数是相对于浏览器右边缘(overlay布局为LTR时)或左边缘(overlay布局为RTL时)的偏移量
width (value: string = ''): this设置overlay pane元素的宽
height (value: string = ''): this设置overlay pane元素的高
centerVertically(offset: string = ''): this让overlay垂直居中。参数可选,是相对于垂直中心的偏移量。
centerHorizontally(offset: string = ''): this让overlay水平居中。参数可选,是相对于水平中心的偏移量。
apply(): void将定位应用到元素上,每当要更新overlay的位置时,都会调用此方法。
dispose():void清除掉定位策略带来的DOM变化

2.5.3 FlexibleConnectedPositionStrategy 灵活连接定位策略

以前的ConnectedPositionStrategy已经废弃了,请使用FlexibleConnectedPositionStrategy。

FlexibleConnectedPositionStrategy 用于相对于页面中其它 "origin"(原点)元素进行定位的浮层。这通常用于菜单、选择器和工具提示。当使用这种连接策略时,会提供一组首选位置,然后根据浮层在视口中的适合程度来选出一个“最佳”位置。

FlexibleConnectedPositionStrategy的特性包括:

  • 能让浮层在其内容到达视口边缘时变得可滚动;
  • 能够在浮层和视口边缘之间配置一个边距;
  • 如果浮层不适合任何一个首选位置,还能把它推入到视口中;
  • 还可以配置在打开浮层时,其大小是否会增长。
  • 这种灵活的定位策略还允许使用 withTransformOriginOn 来基于当前位置设置浮层内元素的 transform-origin。这在对浮层进行动画处理并使动画从它与原点连接的点开始时很有用。

该定位策略提供的方法有:

方法说明
attach (overlayRef: OverlayReference)将此定位策略应用到overlay;给overlay host元素添加上样式cdk-overlay-connected-position-bounding-box;时刻监听浏览器视口变化来更新overlay的定位。
apply()使用相对于原点最适合屏幕的首选位置来更新overlay的定位。定位的选择如下:1.如果任意位置按原样完全适合视口,则选择该定位。2.如果启用了灵活尺寸(flexible dimensions)并且至少有一个满足给定的最小宽度/高度,则选择具有[根据位置权重调整的]最大可用尺寸的位置。3.如果启用了pushing,则选择离开屏幕最少的位置并将其推送到屏幕上。4.如果以上条件均未满足,则使用屏幕外最少的位置。
detach()解除该定位策略
dispose()在overlay元素被销毁时清理掉该定位策略
reapplyLastPosition()将overlay元素与最后触发位置计算的元素重新对齐,即使“首选定位”列表中有更合适的位置。这允许在不改变panel方向的情况下重新对齐panel。
......

Overlay为灵活连接策略提供了两个指令:

指令说明
cdkOverlayOrigin应用于元素上,使其可作为 ConnectedPositionStrategy策略的浮层的原点。
cdkConnectedOverlay使用FlexibleConnectedPositionStrategy策略声明式创建overlay。它提供了很多属性供我们灵活配置:原点、overlay距离原点在x或y轴上的偏移、overlay是否需要幕布、overlay panel的大小参数等。

2.6 滚动策略

2.6.1 ScrollStrategy

ScrollStrategy 配置项决定了浮层如何响应浮层元素外部的滚动。四种可用的滚动策略如下:

滚动策略说明
NoopScrollStrategy是默认选项。该策略什么都不做。
CloseScrollStrategy会在滚动时自动关闭浮层。
BlockScrollStrategy当浮层打开时,BlockScrollStrategy 会阻止页面滚动。注意,某些应用可能会实现特殊或自定义的页面滚动;如果 BlockScrollStrategy 与这种情况冲突,可以通过重新提供带有自定义实现的 BlockScrollStrategy 来覆盖它。
RepositionScrollStrategy会在滚动时重新定位浮层元素。注意,这会对滚动带来一些性能影响 - 用户应该在每个具体应用的上下文中权衡这种代价。

ScrollStrategy 接口支持我们创建自定义滚动策略。每个策略都会注入 ScrollDispatcher(来自 @angular/cdk/scrolling),以便在发生滚动时得到通知。

export interface ScrollStrategy {
  // 启用滚动策略
  enable: () => void;
  // 禁用滚动策略
  disable: () => void;
  // 将这个滚动策略挂载到一个overlay上
  attach: (overlayRef: OverlayReference) => void;
  // 从当前overlay上解除滚动策略
  detach?: () => void;
}

今天的overlay先分享到这里。