当前位置: 移动技术网 > IT编程>脚本编程>AngularJs > 从源码看angular/material2 中 dialog模块的实现方法

从源码看angular/material2 中 dialog模块的实现方法

2017年12月12日  | 移动技术网IT编程  | 我要评论

本文将探讨material2中popup弹窗即其dialog模块的实现。

使用方法

  1. 引入弹窗模块
  2. 自己准备作为模板的弹窗内容组件
  3. 在需要使用的组件内注入 matdialog 服务
  4. 调用 open 方法创建弹窗,并支持传入配置、数据,以及对关闭事件的订阅

深入源码

进入material2的源码,先从 matdialog 的代码入手,找到这个 open 方法:

open<t>(
 componentortemplateref: componenttype<t> | templateref<t>,
 config?: matdialogconfig
): matdialogref<t> {
 // 防止重复打开
 const inprogressdialog = this.opendialogs.find(dialog => dialog._isanimating());
 if (inprogressdialog) {
  return inprogressdialog;
 }
 // 组合配置
 config = _applyconfigdefaults(config);
 // 防止id冲突
 if (config.id && this.getdialogbyid(config.id)) {
  throw error(`dialog with id "${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;
}

总体看来弹窗的发起分为三部曲:

  1. 创建一个弹出层(其实是一个原生dom,起宿主和入口的作用)
  2. 在弹出层上创建弹窗容器组件(负责提供遮罩和弹出动画)
  3. 在弹窗容器中创建传入的弹窗内容组件(负责提供内容)

弹出层的创建

对于其他组件,仅仅封装模板以及内部实现就足够了,最多还要增加与父组件的数据、事件交互,所有这些事情,单使用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;
}

所做的事情无非就是动态创建组件的四步曲:

  1. 创建工厂
  2. 使用工厂创建组件
  3. 将组件整合进appref(同时设置一个移除的方法)
  4. 在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 ?
  portal.viewcontainerref :
  this._viewcontainerref;
 // 在宿主上动态创建组件的代码
 let componentfactory = this._componentfactoryresolver.resolvecomponentfactory(portal.component);
 let ref = viewcontainerref.createcomponent( // 使用 viewcontainerref 动态创建组件到当前视图容器(也就是弹窗容器指令)
  componentfactory, viewcontainerref.length,
  portal.injector || viewcontainerref.parentinjector
 );
 super.setdisposefn(() => ref.destroy());
 this._portal = portal;
 return ref;
}

最后这一步就非常明了了,正是官方文档中使用的动态创建组件的方式(viewcontainerref),至此弹窗已经成功弹出到界面中了。

弹窗的关闭

还有最后一个要注意的点就是弹窗如何关闭,从上文可以知道应该要先执行关闭动画,然后才能销毁弹窗,material2的弹窗容器组件添加了一堆节点:

host: {
 'class': 'mat-dialog-container',
 'tabindex': '-1',
 '[attr.role]': '_config?.role',
 '[attr.aria-labelledby]': '_arialabelledby',
 '[attr.aria-describedby]': '_config?.ariadescribedby || null',
 '[@slidedialog]': '_state',
 '(@slidedialog.start)': '_onanimationstart($event)',
 '(@slidedialog.done)': '_onanimationdone($event)',
}

其中需要关注的就是material2在容器组件中添加了一个动画叫 slidedialog ,并为其设置了动画事件,现在关注动画完成事件的回调:

_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?: any): void {
  this._result = dialogresult; // 把传入的结果赋值给私有变量 _result 以便在上面的 this._afterclosed.next(this._result) 中使用
  // transition the backdrop in parallel to the dialog.
  rxchain.from(this._containerinstance._animationstatechanged)
  .call(filter, event => event.phasename === 'start')
  .call(first)
  .subscribe(() => {
    this._beforeclose.next(dialogresult);
    this._beforeclose.complete();
    this._overlayref.detachbackdrop();
  });
  this._containerinstance._startexitanimation();
}

总结

以上就是整个material2 dialog能力走通的过程,可见即使是 angular 这么完善又庞大的框架,想要完美解耦封装弹窗能力也不能完全避免原生dom操作。

除此之外给我的感觉还有——无论是angular还是material2,它们对typescript的使用都让我自叹不如,包括但不限于抽象类、泛型等装逼技巧,把它们的源码慢慢看下来,着实能学到不少东西。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网