elementUI——MessageBox组件源码分析

说明:以下基于[email protected]

elementUI 弹框示例

从场景上说,MessageBox 的作用是美化系统自带的 alert、confirm 和 prompt,因此适合展示较为简单的内容。如果需要弹出较为复杂的内容,请使用 Dialog。

图1:原生alert

图2:原生confirm

图3:原生prompt

本次主要分析MessageBox以及基于MessageBoxalertconfirmprompt
阅读以下内容的前提是对官网示例和组件用法有了基本了解。

在elementUI的src/index.js中:

  Vue.prototype.$msgbox = MessageBox;
  Vue.prototype.$alert = MessageBox.alert;
  Vue.prototype.$confirm = MessageBox.confirm;
  Vue.prototype.$prompt = MessageBox.prompt;

$msgbox本质上就是MessageBox,而其他三个方法($alert$confirm$prompt)是对MessageBox的再封装。

1. 弹框对应的单文件及基本组成

具体代码见packages/message-box/src/main.vue单文件组件(见源码为方便后文说明,取名msgboxVue)。
整体而言,如下图所示,弹框分三个部分,header(标题+关闭按钮)、content(message+input)和btns(取消+确定)

图4

1.1 聊聊主要涉及哪些options:

  • 整体
    visible:控制整体是否显示,不对外暴露;
    customClass:自定义类名,控制整体的样式;
    center:空控制弹框中各部分是否水平居中显示;
    callback:若不使用 Promise,可以使用此参数指定 MessageBox 关闭后的回调;
  • header
    title:标题;
    showClose:控制header部分的关闭按钮的显示,支持click和enter按键;
  • content:message
    message:消息,通过dangerouslyUseHTMLString来确定是否支持html片段,如果为真,message赋给v-html;
  • content:输入框
    showInput:控制是否显示输入框,prompt模式下,默认为true;
    inputValue:输入框的初始值;
    inputType:输入框的类型,即el-input的type属性;
    inputPlaceholder:输入框的占位符;
    inputPattern:输入框的校验表达式,即正则表达式,例如校验输入值是否是邮箱;
    inputErrorMessage:输入框的输入值校验失败后的显示文字;
  • btns:取消按钮
    cancelButtonClass:取消按钮的自定义类名;cancelButtonLoading:内部option,取消按钮的loading;
    showCancelButton:是否显示取消按钮;
    cancelButtonText:取消按钮的文本内容;
  • btns:确定按钮
    confirmButtonClass:确定按钮的自定义类名;confirmButtonLoading:内部option,确定按钮的loading;
    showConfirmButton:是否显示确定按钮;
    confirmButtonText:确定按钮的文本内容;

1.2 聊聊弹框上下左右居中的样式实现

  • 组件最外层有一类名el-message-box__wrapper,通过fixed position,使得整个弹框组件占据整个屏幕,通过text-align: center,使得核心的el-message-box部分水平居中:
.el-message-box__wrapper {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    text-align: center;
}
图5
  • 跟上图中el-message-box部分并列有一伪元素,样式如下,通过display: inline-block;vertical-align: middle;使得el-message-box部分垂直居中:
.el-message-box__wrapper:after {
    content: "";
    display: inline-block;
    height: 100%;
    width: 0;
    vertical-align: middle;
}

1.3 msgboxVue单文件中混入popup及popup-manager:

1.3.1 PopupManager的作用主要是设置蒙版,当有多级蒙版时,能在此进行统一管理:

  • PopupManager中有一个zIndex属性初始值为2000,所有的弹出框的z-index其实都是从这个PopupManager.zIndex中获取的,当要展示一个新的弹出框时,组件便会去获取最新的PopupManager.zIndex,然后为PopupManager.zIndex加1,这样就保证了新的弹出框总是比旧的弹出框z-index大,省去自己一个个设置的麻烦,也减少问题的出现。
  • 通过类v-modal,设置蒙版样式(黑色半透明)。详见关于使用element中的popup问题。

1.3.2 popup.js是一个mixin混入,详见[element-ui 源码分析-工具篇:popup](https://segmentfault.com/a/1190000020242564),功能清单如下:

  • 引入popupManger
  • beforeMount 周期时,调用PopupManager对象的注册方法
  • beforeDestroy周期中,调用PopupManager对象的注销方法
  • doOpen方法,设置弹窗组件的z-index,调用PopupManager.openModal方法
  • doAfterClose方法,调用PopupManager.closeModal方法

msgboxVue单文件中混入popup,主要用到一个prop和两个方法,由于popup和popup-manager是通用方法,多个组件用到,功能较多,在这里主要介绍在msgboxVue中用到的:

  • visible prop
    visible是布尔类型,结合v-show用于设置msgboxVue的显示与否;
    在popup中watch visible,当值为true时,调用popup中的open方法,否则调用close方法;
  • open方法和doOpen方法
    open方法主要是调用doOpen方法。doOpen
    a.通过PopupManager.openModal产生蒙版并设置样式和层级等;
    b.modal属性来自msgboxVue组件的props,如果为真,当lockScroll(默认为true,见MessageBox 弹框:是否在 MessageBox 出现时将 body 滚动锁定)为true时,需要做到body上的滚动条被禁止,这里有个小技巧:当body上有竖向滚动条时,获取滚动条宽度scrollBarWidth(方法见elementUI——scrollbar-width获取滚动条宽度,笔者电脑chrome浏览器上为17px),通过class el-popup-parent--hidden设置overflow:hidden,使得滚动条隐藏和失效,同时设置body的padding Right += scrollBarWidth,这个是为了保证页面不至于因为竖向滚动条消失,而发生抖动。
    c.设置当前当前弹框position为absolute,并设置zIndex为PopupManager.nextZIndex(),即比蒙版的zIndex大一;
    d.设置_closing为false、opened为true、_opening为false;
  • close方法和doClose方法
    close方法主要是调用doClose方法。doClose
    a. 恢复body样式,如重新显示滚动条、恢复body原有的paddingRight;
    b. 通过PopupManager.closeModal关闭蒙版。

1.4 msgboxVue单文件:
在上一小节聊完了重要的且相对独立的popup及popup-manager,接下来聊聊msgboxVue组件的其他功能。
1.4.1 图4中弹框右上角关闭按钮点击事件

图6:弹框右上角关闭按钮执行流程

流程走到最后会执行doClose方法,这个在1.3.2最后已介绍。
1.4.2 图4中input框enter事件和“确定”按钮点击事件

@keydown.enter.native="handleInputEnter"
handleInputEnter() {
        if (this.inputType !== 'textarea') {
          return this.handleAction('confirm');
        }
}

可以发现,最后会执行handleAction方法,跟上一小节一样,只不过action变成了“confirm”,如果校验合法,会关闭弹框和蒙版。
1.4.3 图4中弹框取消按钮点击事件

@click.native="handleAction('cancel')"

同上,只不过action变成了“cancel”。
讲到这,msgboxVue单文件就基本讲完了,接下来就是另一个重头戏——对msgboxVue单文件的二次封装:$msgbox$alert$confirm$prompt

2. 对msgboxVue单文件组件的封装:MessageBox(即$msgbox)

在官方示例中,可以发现,可以通过函数调用的方式生成弹框(本质是对第1节中msgboxVue单文件组件的调用),如果支持promise,当点击“确定”按钮时,执行then方法;当点击“取消”按钮或右上角关闭按钮时,执行catch方法;或者通过传入callback回调函数的方式,来处理“确定”、“取消”和“关闭”等。
alert、prompt)是对MessageBox的再封装,在这里首先分析MessageBox。
先上MessageBox`源码:

const MessageBox = function(options, callback) {
  if (Vue.prototype.$isServer) return;
  if (typeof options === 'string' || isVNode(options)) {
    options = {
      message: options
    };
    if (typeof arguments[1] === 'string') {
      options.title = arguments[1];
    }
  } else if (options.callback && !callback) {
    callback = options.callback;
  }

  if (typeof Promise !== 'undefined') {
    return new Promise((resolve, reject) => {
      msgQueue.push({
        options: merge({}, defaults, MessageBox.defaults, options),
        callback: callback,
        resolve: resolve,
        reject: reject
      });

      showNextMsg();
    });
  } else {
    msgQueue.push({
      options: merge({}, defaults, MessageBox.defaults, options),
      callback: callback
    });

    showNextMsg();
  }
};

上面代码,主要做两件事:
a. 对messagecallback做处理
b. msgQueue数组保存options和callback,如果浏览器支持Promise,那么再将resovlereject封装进msgQueue,分别触发后续thencatch逻辑,可见下面的官方示例:
// 官方示例




图7:messagebox流程图

其中callback,如果没有往messagebox中传入callback,则使用默认值defaultCallback,如果支持promise,当action为 'confirm'(点击确认按钮或input框按enter键)时,调用下一步的then方法;当action为'cancel'或'close'(点击取消或关闭按钮)时,调用下一步的catch方法。其实现如下:

const defaultCallback = action => {
  if (currentMsg) { // 从msgQueue数组头部取出
    let callback = currentMsg.callback;
    if (typeof callback === 'function') { // 调用用户传入的callback
      if (instance.showInput) {
        callback(instance.inputValue, action);
      } else {
        callback(action);
      }
    }
    if (currentMsg.resolve) {
      if (action === 'confirm') {
        if (instance.showInput) {
          currentMsg.resolve({ value: instance.inputValue, action }); // 如果支持promise,当action为 'confirm'时,调用下一步的then方法
        } else {
          currentMsg.resolve(action);
        }
      } else if (currentMsg.reject && (action === 'cancel' || action === 'close')) {
        currentMsg.reject(action); // 如果支持promise,当action为'cancel'或'close'时,调用下一步的catch方法
      }
    }
  }
};

$alert$confirm$prompt是对MessageBox的再次简单封装,例如:

// element-ui\packages\message-box\src\main.js
MessageBox.prompt = (message, title, options) => {
  if (typeof title === 'object') {
    options = title;
    title = '';
  } else if (title === undefined) {
    title = '';
  }
  return MessageBox(merge({
    title: title,
    message: message,
    showCancelButton: true,
    showInput: true,
    $type: 'prompt'
  }, options));
};

// element-ui\src\index.js
Vue.prototype.$prompt = MessageBox.prompt;

最后,举一反三,Loading、Notification和Message也有类似做法,通过对组件进行二次封装,对外提供了函数调用方式。

  Vue.prototype.$loading = Loading.service;
  Vue.prototype.$notify = Notification;
  Vue.prototype.$message = Message;

你可能感兴趣的:(elementUI——MessageBox组件源码分析)