美文网首页前端开发elementUI
elementUI——MessageBox组件源码分析

elementUI——MessageBox组件源码分析

作者: videring | 来源:发表于2020-05-17 02:56 被阅读0次

    说明:以下基于elementUI@2.13.1。

    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回调函数的方式,来处理“确定”、“取消”和“关闭”等。
    msgbox本质上就是MessageBox,而其他三个方法(alert、confirm和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逻辑,可见下面的官方示例:
    // 官方示例

    <template>
      <el-button type="text" @click="open">点击打开 Message Box</el-button>
    </template>
    
    <script>
      export default {
        methods: {
          open() {
            const h = this.$createElement;
            this.$msgbox({
              title: '消息',
              message: h('p', null, [
                h('span', null, '内容可以是 '),
                h('i', { style: 'color: teal' }, 'VNode')
              ]),
              showCancelButton: true,
              confirmButtonText: '确定',
              cancelButtonText: '取消',
              beforeClose: (action, instance, done) => {
                if (action === 'confirm') {
                  instance.confirmButtonLoading = true;
                  instance.confirmButtonText = '执行中...';
                  setTimeout(() => {
                    done();
                    setTimeout(() => {
                      instance.confirmButtonLoading = false;
                    }, 300);
                  }, 3000);
                } else {
                  done();
                }
              }
            }).then(action => {
              this.$message({
                type: 'info',
                message: 'action: ' + action
              });
            });
          }
        }
      }
    </script>
    
    图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组件源码分析

        本文链接:https://www.haomeiwen.com/subject/eccsnhtx.html