Angular Material —— MatSnackModule 源码解析(一)

Angular Material —— MatSnackModule 源码解析(一)

yande.re 309004

惯例一张图,Angular Material版本为7.0.2,之后的文章版本默认为这个,以下开始正题。


首先,在官网上可以看到该模块有两种使用方式:

 // Simple message.
 let snackBarRef = snackBar.open('Message archived');

 // Simple message with an action.
 let snackBarRef = snackBar.open('Message archived', 'Undo');

 // Load the given component into the snack-bar.
 let snackBarRef = snackbar.openFromComponent(MessageArchivedComponent);

第二行跟第五行实际上是一样的,为第一种使用方式,第八行为第二种使用方式。

我们先来看第一种使用方式:

    /**
     * Opens a snackbar with a message and an optional action.
     * @param message The message to show in the snackbar.
     * @param action The label for the snackbar action.
     * @param config Additional configuration options for the snackbar.
     */
    open(message: string, action: string = '', config?: MatSnackBarConfig):
        MatSnackBarRef<SimpleSnackBar {
      const _config = {...this._defaultConfig, ...config};

      // Since the user doesn't have access to the component, we can
      // override the data to pass in our own message and action.
      _config.data = {message, action};

      if (!_config.announcementMessage) {
        _config.announcementMessage = message;
      }

      return this.openFromComponent(SimpleSnackBar, _config);

  }

我们实际上调用的是该方法,我们可以发现,实际上还有第三个参数可以传入,该参数是该SnackBar的一些配置,具体如下

 /** Injection token that can be used to access the data that was passed in to a snack bar. */
 export const MAT_SNACK_BAR_DATA = new InjectionToken<any('MatSnackBarData');

 /** Possible values for horizontalPosition on MatSnackBarConfig. */
 export type MatSnackBarHorizontalPosition = 'start' | 'center' | 'end' | 'left' | 'right';

 /** Possible values for verticalPosition on MatSnackBarConfig. */
 export type MatSnackBarVerticalPosition = 'top' | 'bottom';

 /**
  * Configuration used when opening a snack-bar.
  */
 export class MatSnackBarConfig<D = any {
   /** The politeness level for the MatAriaLiveAnnouncer announcement. */
   politeness?: AriaLivePoliteness = 'assertive';

   /**
    * Message to be announced by the LiveAnnouncer. When opening a snackbar without a custom
    * component or template, the announcement message will default to the specified message.
    */
   announcementMessage?: string = '';

   /** The view container to place the overlay for the snack bar into. */
   viewContainerRef?: ViewContainerRef;

   /** The length of time in milliseconds to wait before automatically dismissing the snack bar. */
   duration?: number = 0;

   /** Extra CSS classes to be added to the snack bar container. */
   panelClass?: string | string[];

   /** Text layout direction for the snack bar. */
   direction?: Direction;

   /** Data being injected into the child component. */
   data?: D | null = null;

   /** The horizontal position to place the snack bar. */
   horizontalPosition?: MatSnackBarHorizontalPosition = 'center';

   /** The vertical position to place the snack bar. */
   verticalPosition?: MatSnackBarVerticalPosition = 'bottom';
 }

回到正题,首先,合并配置参数

 const _config = {...this._defaultConfig, ...config};

因为使用的是MatSnackModule自己的组件,所以,会将用户在config里传入的data属性进行覆盖

  _config.data = {message, action};

第一个参数为显示的内容, 第二个则是按钮的值。 接着判断config里的anniuncementMessage是否为空,为空的话,则把message的值赋给它;

 if (!_config.announcementMessage) {
      _config.announcementMessage = message;
 }

最后,在内部使用openFromCompoent方法打开自带的组件;

 return this.openFromComponent(SimpleSnackBar, _config);

以上,便是open方法的简单说明,实际上,只是将我们传入的数据进行转换,最后使用openFromCompoent调用自带的组件进行显示;

openFromCompoent这个方法实际上也很简单,调用了内部的私有方法_attach

   /**
    * Creates and dispatches a snack bar with a custom component for the content, removing any
    * currently opened snack bars.
    *
    * @param component Component to be instantiated.
    * @param config Extra configuration for the snack bar.
    */
   openFromComponent<T(component: ComponentType<T, config?: MatSnackBarConfig):
     MatSnackBarRef<T {
     return this._attach(component, config) as MatSnackBarRef<T;
   }

因此,无论我们是使用哪一种方法来显示,实际上最终都是在内部调用_attch方法; 在这里补充说明一下,MatSnackModule实际上是调用了CDK里的OverlayModulePortalModule,前者是生成一个显示层,后者则是把前者生成的显示层挂载到DOM里;

 /**
   * Places a new component or a template as the content of the snack bar container.
   */
  private _attach<T(content: ComponentType<T | TemplateRef<T, userConfig?: MatSnackBarConfig):
    MatSnackBarRef<T | EmbeddedViewRef<any {

    const config = {...new MatSnackBarConfig(), ...this._defaultConfig, ...userConfig};
    const overlayRef = this._createOverlay(config);
    const container = this._attachSnackBarContainer(overlayRef, config);
    const snackBarRef = new MatSnackBarRef<T | EmbeddedViewRef<any(container, overlayRef);

    if (content instanceof TemplateRef) {
      const portal = new TemplatePortal(content, null!, {
        $implicit: config.data,
        snackBarRef
      } as any);

      snackBarRef.instance = container.attachTemplatePortal(portal);
    } else {
      const injector = this._createInjector(config, snackBarRef);
      const portal = new ComponentPortal(content, undefined, injector);
      const contentRef = container.attachComponentPortal<T(portal);

      // We can't pass this via the injector, because the injector is created earlier.
      snackBarRef.instance = contentRef.instance;
    }

    // Subscribe to the breakpoint observer and attach the mat-snack-bar-handset class as
    // appropriate. This class is applied to the overlay element because the overlay must expand to
    // fill the width of the screen for full width snackbars.
    this._breakpointObserver.observe(Breakpoints.Handset).pipe(
      takeUntil(overlayRef.detachments().pipe(take(1)))
    ).subscribe(state = {
      if (state.matches) {
        overlayRef.overlayElement.classList.add('mat-snack-bar-handset');
      } else {
        overlayRef.overlayElement.classList.remove('mat-snack-bar-handset');
      }
    });

    this._animateSnackBar(snackBarRef, config);
    this._openedSnackBarRef = snackBarRef;
    return this._openedSnackBarRef;
  }

以上是_attch的代码,下面开始解释。 首先从传入的第一个参数上来看,可以知道支持传入ComponentType或者TemplateRef;因此,除了最上面的三种官网所说的三种使用方法外,还可以使用openFromTemplateRef来进行模板的显示;

  openFromTemplate(template: TemplateRef<any, config?: MatSnackBarConfig):
    MatSnackBarRef<EmbeddedViewRef<any {
    return this._attach(template, config);
  }

回到正题,首先,合并config;接着,调用_createOverlay来创建显示层;

 /**
   * Creates a new overlay and places it in the correct location.
   * @param config The user-specified snack bar config.
   */
  private _createOverlay(config: MatSnackBarConfig): OverlayRef {
    const overlayConfig = new OverlayConfig();
    overlayConfig.direction = config.direction;

    let positionStrategy = this._overlay.position().global();
    // Set horizontal position.
    const isRtl = config.direction === 'rtl';
    const isLeft = (
      config.horizontalPosition === 'left' ||
      (config.horizontalPosition === 'start' && !isRtl) ||
      (config.horizontalPosition === 'end' && isRtl));
    const isRight = !isLeft && config.horizontalPosition !== 'center';
    if (isLeft) {
      positionStrategy.left('0');
    } else if (isRight) {
      positionStrategy.right('0');
    } else {
      positionStrategy.centerHorizontally();
    }
    // Set horizontal position.
    if (config.verticalPosition === 'top') {
      positionStrategy.top('0');
    } else {
      positionStrategy.bottom('0');
    }

    overlayConfig.positionStrategy = positionStrategy;
    return this._overlay.create(overlayConfig);
  }

这里首先是创建一个新的overlay的默认配置对象;接着根据传入的config来进行合并,首先是直接覆盖direction,这个是代表文字的显示方向,属性值为rtlltr;接下来是创建位置信息的属性,这个是用来进行设置SnackBar显示的显示位置;接下来则是根据传入的config来判断snackBar是显示在左边还是右边还是中间;接着进行判断是显示在页面的顶部还是页面的底部,最后,把这个位置的配置赋给overlayConfig.positionStrategy,最后根据这个配置信息创建一个OverlayRef;

 /**
   * Creates an overlay.
   * @param config Configuration applied to the overlay.
   * @returns Reference to the created overlay.
   */
  create(config?: OverlayConfig): OverlayRef {
    // 创建HostElement;
    // _createHostElement里面通过依赖注入获取对应的document;
    // 接着创建一个全新的div元素;
    // 然后通过 this._overlayContainer.getContainerElement().appendChild(host);
    // 获取对应的父级元素,接着把上面的div添加到里面去
    // 而 this._overlayContainer.getContainerElement()这个也很简单
    // 首先判断内部的_containerElement是否存在,如果存在的话则直接返回,
    // 不存在的话则调用内部的_createContainer方法;
    // 这个方法也很简单,直接通过注入的document来创建一个div,然后给这个div添加‘cdk-overlay-container’这个class名,最后添加到document.body里面,接着把这个div赋给_containerElement;
    const host = this._createHostElement();
    // 这个也很简单,在内部创建一个div,然后将该div的id设置为`cdk-overlay-${nextUniqueId++}`;
    // nextUniqueId默认是0;这个是为了解决多个overlay的情况下的问题;
    // 然后,给这个div添加'cdk-overlay-pane'这个class,最后,把这个div添加到host(即传入的值)里    
    const pane = this._createPaneElement(host);
    // 在内部,首先判断_appRef是否存在,不存在的话通过注入器获取‘ApplicationRef’;
    // 接着创建一个DomPortalOutlet的实例;
    // DomPortalOutlet是为了能在Angular上下文之外挂载该视图(附加dom)
    // 这里还是推介大家去看一下这个大大写的帖子(https://juejin.im/post/5b0d119a518825158e173f97)
    const portalOutlet = this._createPortalOutlet(pane);
    // 配置对象
    const overlayConfig = new OverlayConfig(config);

    overlayConfig.direction = overlayConfig.direction || this._directionality.value;
    return new OverlayRef(portalOutlet, host, pane, overlayConfig, this._ngZone,
      this._keyboardDispatcher, this._document, this._location);
  }

上面是this._overlay.create的解析;

接下来说下OverlayRef.attchDomPortalOutlet.attch方法,下面会用到;

首先是DomPortalOutlet.attch;

export class DomPortalOutlet extends BasePortalOutlet {
 constructor(
      /** Element into which the content is projected. */
      public outletElement: Element,
      private _componentFactoryResolver: ComponentFactoryResolver,
      private _appRef: ApplicationRef,
      private _defaultInjector: Injector) {
    super();
  }
  // ………… 其他的方法
 }
 // DomPortalOutlet.attch实际上是调用了BasePortalOutlet.attch,下面是BasePortalOutlet.attch的代码;
      attach<T>(portal: ComponentPortal<T>): ComponentRef<T>;
  attach<T>(portal: TemplatePortal<T>): EmbeddedViewRef<T>;
  attach(portal: any): any;

  /** Attaches a portal. */
  attach(portal: Portal<any>): any {
    if (!portal) {
      throwNullPortalError();
    }
    // this.hasAttached()源码如下,很简单,不做解释
    //  hasAttached(): boolean {
    //      return !!this._attachedPortal;
    //  }
    // 是否已经有了添加的Portal;
    if (this.hasAttached()) {
      throwPortalAlreadyAttachedError();
    }
    // 自身是否被销毁了;
    if (this._isDisposed) {
      throwPortalOutletAlreadyDisposedError();
    }
    // 根据对应的类型,把portal附加到视图(PortalOutlet)中;
    if (portal instanceof ComponentPortal) {
      this._attachedPortal = portal;
      return this.attachComponentPortal(portal);
    } else if (portal instanceof TemplatePortal) {
      this._attachedPortal = portal;
      return this.attachTemplatePortal(portal);
    }

    throwUnknownPortalTypeError();
  }
  // 而对应的attachComponentPortal以及attachTemplatePortal都是抽象方法;

  // abstract attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T>;

  // abstract attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C>;

  // 以下 是DomPortalOutlet里的实现方法;
  /**
   * Attach the given ComponentPortal to DOM element using the ComponentFactoryResolver.
   * @param portal Portal to be attached
   * @returns Reference to the created component.
   */
  attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
    // 优先使用自定义的componentFactoryResolver,如果没有的话使用默认的;
    const resolver = portal.componentFactoryResolver || this._componentFactoryResolver;
    // 解析出对应的component
    const componentFactory = resolver.resolveComponentFactory(portal.component);
    let componentRef: ComponentRef<T>;

    // If the portal specifies a ViewContainerRef, we will use that as the attachment point
    // for the component (in terms of Angular's component tree, not rendering).
    // When the ViewContainerRef is missing, we use the factory to create the component directly
    // and then manually attach the view to the application.
    // 如果指定了viewContainerRef(即附加到的视图容器),那么将其附加到上去
    if (portal.viewContainerRef) {
      componentRef = portal.viewContainerRef.createComponent(
          componentFactory,
          portal.viewContainerRef.length,
          portal.injector || portal.viewContainerRef.injector);
      // 销毁时的处理函数
      this.setDisposeFn(() => componentRef.destroy());
    } else {
      // 如果没有指定ViewContainerRef的话,那么首先创建组件视图
      componentRef = componentFactory.create(portal.injector || this._defaultInjector);
      // 把componmentRef的挂载到视图树上;
      // ps:就是挂载到angular的根视图上;
      this._appRef.attachView(componentRef.hostView);
      // 销毁时的处理函数
      // 因为挂载到了视图树上,所以销毁是除了销毁组件视图外,还需要从组件树中把
      this.setDisposeFn(() => {
        this._appRef.detachView(componentRef.hostView);
        componentRef.destroy();
      });
    }
    // At this point the component has been instantiated, so we move it to the location in the DOM
    // where we want it to be rendered.
    // 把组件渲染出来(即添加到DOM里)
    this.outletElement.appendChild(this._getComponentRootNode(componentRef));

    return componentRef;
  }

  /**
   * Attaches a template portal to the DOM as an embedded view.
   * @param portal Portal to be attached.
   * @returns Reference to the created embedded view.
   */
  attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {
    // 创建视图容器
    let viewContainer = portal.viewContainerRef;
    let viewRef = viewContainer.createEmbeddedView(portal.templateRef, portal.context);
    // 手动强制进行变更检测
    viewRef.detectChanges();

    // The method `createEmbeddedView` will add the view as a child of the viewContainer.
    // But for the DomPortalOutlet the view can be added everywhere in the DOM
    // (e.g Overlay Container) To move the view to the specified host element. We just
    // re-append the existing root nodes.
    // 添加到dom里
    viewRef.rootNodes.forEach(rootNode => this.outletElement.appendChild(rootNode));

    this.setDisposeFn((() => {
      let index = viewContainer.indexOf(viewRef);
      if (index !== -1) {
        viewContainer.remove(index);
      }
    }));

    // TODO(jelbourn): Return locals from view.
    return viewRef;
  }

这次先写到到这里,剩下的有空再写;

阅读:
553
评论:
0
转载:
0
时间:
2018-10-29 17:20

×你还没有登录,登陆后有更多特权喔!点我登录