本文将探讨material2中popup弹窗即其Dialog模块的实现。
使用方法
- 引入弹窗模块
- 自己准备作为模板的弹窗内容组件
- 在需要使用的组件内注入 MatDialog 服务
- 调用 open 方法创建弹窗,并支持传入配置、数据,以及对关闭事件的订阅
深入源码
进入material2的源码,先从 MatDialog 的代码入手,找到这个 open 方法:
open<T>( componentOrTemplateRef: ComponentType<T> | TemplateRef<T>, config"${config.id}" exists already. The dialog id must be unique.`); } // 第一步:创建弹出层 const overlayRef = this._createOverlay(config); // 第二步:在弹出层上添加弹窗容器 const dialogContainer = this._attachDialogContainer(overlayRef, config); // 第三步:把传入的组件添加到创建的弹出层中创建的弹窗容器中 const dialogRef = this._attachDialogContent(componentOrTemplateRef, dialogContainer, overlayRef, config); // 首次弹窗要添加键盘监听 if (!this.openDialogs.length) { document.addEventListener('keydown', this._boundKeydown); } // 添加进队列 this.openDialogs.push(dialogRef); // 默认添加一个关闭的订阅 关闭时要移除此弹窗 // 当是最后一个弹窗时触发全部关闭的订阅并移除键盘监听 dialogRef.afterClosed().subscribe(() => this._removeOpenDialog(dialogRef)); // 触发打开的订阅 this.afterOpen.next(dialogRef); return dialogRef; }
总体看来弹窗的发起分为三部曲:
- 创建一个弹出层(其实是一个原生DOM,起宿主和入口的作用)
- 在弹出层上创建弹窗容器组件(负责提供遮罩和弹出动画)
- 在弹窗容器中创建传入的弹窗内容组件(负责提供内容)
弹出层的创建
对于其他组件,仅仅封装模板以及内部实现就足够了,最多还要增加与父组件的数据、事件交互,所有这些事情,单使用angular Component就足够实现了,在何处使用就将组件选择器放到哪里去完事。
但对于弹窗组件,事先并不知道会在何处使用,因此不适合实现为一个组件后通过选择器安放到页面的某处,而应该将其作为弹窗插座放置到全局,并通过服务来调用。
material2也要面临这个问题,这个弹窗插座是避免不了的,那就在内部实现它,在实际调用弹窗方法时动态创建这个插座就可以了。要实现效果是:对用户来说只是在单纯调用一个 open 方法,由material2内部来创建一个弹出层,并在这个弹出层上创建弹窗。
找到弹出层的创建代码如下:
create(config: OverlayConfig = defaultConfig): OverlayRef { const pane = this._createPaneElement(); // 弹出层DOM 将被添加到宿主DOM中 const portalHost = this._createPortalHost(pane); // 宿主DOM 将被添加到<body>末端 return new OverlayRef(portalHost, pane, config, this._ngZone); // 弹出层的引用 } private _createPaneElement(): HTMLElement { let pane = document.createElement('div'); pane.id = `cdk-overlay-${nextUniqueId++}`; pane.classList.add('cdk-overlay-pane'); this._overlayContainer.getContainerElement().appendChild(pane); // 将创建好的带id的弹出层添加到宿主 return pane; } private _createPortalHost(pane: HTMLElement): DomPortalHost { // 创建宿主 return new DomPortalHost(pane, this._componentFactoryResolver, this._appRef, this._injector); }
其中最关键的方法其实是 getContainerElement() , material2把最"丑"最不angular的操作放在了这里面,看看其实现:
getContainerElement(): HTMLElement { if (!this._containerElement) { this._createContainer(); } return this._containerElement; } protected _createContainer(): void { let container = document.createElement('div'); container.classList.add('cdk-overlay-container'); document.body.appendChild(container); // 在body下创建顶层的宿主 姑且称之为弹出层容器(OverlayContainer) this._containerElement = container; }
弹窗容器的创建
跳过其他细节,现在得到了一个弹出层引用 overlayRef。material2接下来给它添加了一个弹窗容器组件,这个组件是material2自己写的一个angular组件,打开弹窗时的遮罩部分以及弹窗的外轮廓其实就是这个组件,对于为何要再套这么一层容器,有其一些考虑。
动画效果的保护
这样动态创建的组件有一个缺点,那就是其销毁是无法触发angular动画的,因为一瞬间就销毁掉了,所以material2为了实现动画效果,多加了这么一个容器来实现动画,在关闭弹窗时,实际上是在播放弹窗的关闭动画,然后监听容器的动画状态事件,在完成关闭动画后才执行销毁弹窗的一系列代码,这个过程与其为难用户来实现,不如自己给封装了。
注入服务的保护
目前版本的angular关于在动态创建的组件中注入服务还存在一个注意点,就是直接创建出的组件无法使用隐式的依赖注入,也就是说,直接在组件的 constructor 中声明服务对象的实例是不起作用的,而必须先注入 Injector ,再使用这个 Injector 把注入的服务都 get 出来:
private 服务;
constructor( private injector: Injector // private 服务: 服务类 // 这样是无效的 ) { this.服务 = injector.get('服务类名'); }
解决的办法是不直接创建出组件来注入服务,而是先创建一个指令,再在这个指令中创建组件并注入服务使用,这时隐式的依赖注入就又有效了,material2就是这么干的:
<ng-template cdkPortalHost></ng-template>
其中的 cdkPortalHost 指令就是用来后续创建组件的。
所以创建这么一个弹窗容器组件,用户就感觉不到这一点,很顺利的像普通组件一样注入服务并使用。
创建弹窗容器的核心方法在 dom-portal-host.ts 中:
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> { // 创建工厂 let componentFactory = this._componentFactoryResolver.resolveComponentFactory(portal.component); let componentRef: ComponentRef<T>; if (portal.viewContainerRef) { componentRef = portal.viewContainerRef.createComponent( componentFactory, portal.viewContainerRef.length, portal.injector || portal.viewContainerRef.parentInjector); this.setDisposeFn(() => componentRef.destroy()); // 暂不知道为何有指定宿主后面还要把它添加到宿主元素DOM中 } else { componentRef = componentFactory.create(portal.injector || this._defaultInjector); this._appRef.attachView(componentRef.hostView); this.setDisposeFn(() => { this._appRef.detachView(componentRef.hostView); componentRef.destroy(); }); // 到这一步创建出了经angular处理的DOM } // 将创建的弹窗容器组件直接append到弹出层DOM中 this._hostDomElement.appendChild(this._getComponentRootNode(componentRef)); // 返回组件的引用 return componentRef; }
所做的事情无非就是动态创建组件的四步曲:
- 创建工厂
- 使用工厂创建组件
- 将组件整合进AppRef(同时设置一个移除的方法)
- 在DOM中插入这个组件的原始节点
弹窗内容
从上文可以知道,得到的弹窗容器组件中存在一个宿主指令,实际上是在这个宿主指令中创建弹窗内容组件。进入宿主指令的代码可以找到 attachComponentPortal 方法:
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> { portal.setAttachedHost(this); // If the portal specifies an origin, use that as the logical location of the component // in the application tree. Otherwise use the location of this PortalHost. // 如果入口已经有宿主则使用那个宿主 // 否则使用 PortalHost 作为宿主 let viewContainerRef = portal.viewContainerRef != null "color: #ff0000">弹窗的关闭还有最后一个要注意的点就是弹窗如何关闭,从上文可以知道应该要先执行关闭动画,然后才能销毁弹窗,material2的弹窗容器组件添加了一堆节点:
host: { 'class': 'mat-dialog-container', 'tabindex': '-1', '[attr.role]': '_config"htmlcode">_onAnimationDone(event: AnimationEvent) { if (event.toState === 'enter') { this._trapFocus(); } else if (event.toState === 'exit') { this._restoreFocus(); } this._animationStateChanged.emit(event); this._isAnimating = false; }这里发射了这个事件,并在 MatDialogRef 中订阅:
constructor( private _overlayRef: OverlayRef, private _containerInstance: MatDialogContainer, public readonly id: string = 'mat-dialog-' + (uniqueId++) ) { // 添加弹窗开启的订阅 这里的 RxChain 是material2自己对rxjs的工具类封装 RxChain.from(_containerInstance._animationStateChanged) .call(filter, event => event.phaseName === 'done' && event.toState === 'enter') .call(first) .subscribe(() => { this._afterOpen.next(); this._afterOpen.complete(); }); // 添加弹窗关闭的订阅,并且需要在收到回调后销毁弹窗 RxChain.from(_containerInstance._animationStateChanged) .call(filter, event => event.phaseName === 'done' && event.toState === 'exit') .call(first) .subscribe(() => { this._overlayRef.dispose(); this._afterClosed.next(this._result); this._afterClosed.complete(); this.componentInstance = null!; }); } /** * 这个也就是实际使用时的关闭方法 * 所做的事情是添加beforeClose的订阅并执行 _startExitAnimation 以开始关闭动画 * 底层做的事是 改变了弹窗容器中 slideDialog 的状态值 */ close(dialogResult"color: #ff0000">总结以上就是整个material2 dialog能力走通的过程,可见即使是 angular 这么完善又庞大的框架,想要完美解耦封装弹窗能力也不能完全避免原生DOM操作。
除此之外给我的感觉还有——无论是angular还是material2,它们对TypeScript的使用都让我自叹不如,包括但不限于抽象类、泛型等装逼技巧,把它们的源码慢慢看下来,着实能学到不少东西。
风云阁资源网 Design By www.bgabc.com
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。
更新日志
- 中国武警男声合唱团《辉煌之声1天路》[DTS-WAV分轨]
- 紫薇《旧曲新韵》[320K/MP3][175.29MB]
- 紫薇《旧曲新韵》[FLAC/分轨][550.18MB]
- 周深《反深代词》[先听版][320K/MP3][72.71MB]
- 李佳薇.2024-会发光的【黑籁音乐】【FLAC分轨】
- 后弦.2012-很有爱【天浩盛世】【WAV+CUE】
- 林俊吉.2012-将你惜命命【美华】【WAV+CUE】
- 晓雅《分享》DTS-WAV
- 黑鸭子2008-飞歌[首版][WAV+CUE]
- 黄乙玲1989-水泼落地难收回[日本天龙版][WAV+CUE]
- 周深《反深代词》[先听版][FLAC/分轨][310.97MB]
- 姜育恒1984《什么时候·串起又散落》台湾复刻版[WAV+CUE][1G]
- 那英《如今》引进版[WAV+CUE][1G]
- 蔡幸娟.1991-真的让我爱你吗【飞碟】【WAV+CUE】
- 群星.2024-好团圆电视剧原声带【TME】【FLAC分轨】