Angular Material —— MatSnackModule 源码解析(一)
Angular Material —— MatSnackModule 源码解析(一)
惯例一张图,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里的OverlayModule
和PortalModule
,前者是生成一个显示层,后者则是把前者生成的显示层挂载到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
,这个是代表文字的显示方向,属性值为rtl
和ltr
;接下来是创建位置信息的属性,这个是用来进行设置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.attch
和DomPortalOutlet.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;
}
这次先写到到这里,剩下的有空再写;