ElementUI源码分析三:MessageBox弹窗组件

7,903 阅读6分钟

“ 本文正在参加「金石计划 . 瓜分6万现金大奖」

文章基于"version": "2.15.12"学习,如有出入请以最新版本为准

一 组件功能

MessageBox官网链接
官方上说到,MessageBox弹窗是为了美化系统自带的alert confirm,peompt,用于消息提示,确认消息和提交内容。源码中体现在src/index.js中的200-205行。代码如下:

const install = function(Vue, opts = {}) {
 ...
  Vue.prototype.$loading = Loading.service;
  Vue.prototype.$msgbox = MessageBox;
  Vue.prototype.$alert = MessageBox.alert;
  Vue.prototype.$confirm = MessageBox.confirm;
  Vue.prototype.$prompt = MessageBox.prompt;
  Vue.prototype.$notify = Notification;
  Vue.prototype.$message = Message;

};

msgbox本质上就是MessageBox,而其他三个方法(msgbox本质上就是MessageBox,而其他三个方法(alert、confirmconfirm和prompt)是对MessageBox的再封装。
从官网来看主要功能分别有:
1 消息提示
2 确认消息
3 提交内容
4 自定义
5 使用html片段
6 区分取消与关闭
7 居中布局
那么就按照功能来依次分析element源码是如何实现的

二 源码分析

为了研究出上述的功能咱们实现的,先把整体的代码过一遍
MessageBox的源码地址为 packages/message-box文件夹,不知道如何看全部element源码的同学可看上一篇文章:
文件夹下主要有三个文件
image.png
其中index.js为注册组件:向外导出了一个MessageBox组件 最终会在 src/index.js中导入使用
image.png
在main.vue文件中。结构上看template分为三部分:
分别是:el-message-box__header;el-message-box__content;el-message-box__btns 对应着下图
image.png
image.png
代码注释如下:

<template>
<!--   最外层的动画标签 -->
  <transition name="msgbox-fade">
    <!--包裹弹框的div;   tabindex="-1" 使用tab键获取不到焦点 但是可以使用js获取-->
    <div
      class="el-message-box__wrapper"
      tabindex="-1"
      v-show="visible"
      @click.self="handleWrapperClick"
      role="dialog"
      aria-modal="true"
      :aria-label="title || 'dialog'">
      <!--中间的弹框-->
      <div class="el-message-box" :class="[customClass, center && 'el-message-box--center']">
        <!--弹窗头部,包含:标题和关闭按钮;title必须设置,如果不设置不显示头部信息-->
        <div class="el-message-box__header" v-if="title !== null">
          <!--头部标题-->
          <div class="el-message-box__title">
            <!--center为true时,为居中布局,可设置图标,图标和标题居中显示-->
            <div
              :class="['el-message-box__status', icon]"
              v-if="icon && center">
            </div>
            <span>{{ title }}</span>
          </div>
          <!--头部关闭按钮-->
<!--           这里的handleAction事件 很多地方用到 传递的参数不一样  close  cancel confirm -->
<!--           aria-label是一个属性 读屏软件可以识别出这是一个关闭按钮 -->
          <button
            type="button"
            class="el-message-box__headerbtn"
            aria-label="Close"
            v-if="showClose"
            @click="handleAction(distinguishCancelAndClose ? 'close' : 'cancel')"
            @keydown.enter="handleAction(distinguishCancelAndClose ? 'close' : 'cancel')"> 
<!--           这里面的class样式前面讲到了 都全部放在theme文件夹中对应的scss文件方便管理和全局换肤 -->
            <i class="el-message-box__close el-icon-close"></i>
          </button>
        </div>
        <!--弹框内容部分-->
        <div class="el-message-box__content">
            <!--消息类型的图标-->
          <div
            :class="['el-message-box__status', icon]"
            v-if="icon && !center && message !== ''">
          </div>
            <!--弹框的主要内容-->
          <div class="el-message-box__message" v-if="message !== ''">
            <slot>
              <!--dangerouslyUseHTMLString是否将 message 属性作为 HTML 片段处理,如果该字段不存在时,直接显示message-->
              <p v-if="!dangerouslyUseHTMLString">{{ message }}</p>
              <!--如果存在将message作为HTML处理-->
              <p v-else v-html="message"></p>
            </slot>
          </div>
          <!--输入框部分,根据设置的showInput显示-->
          <div class="el-message-box__input" v-show="showInput">
            <el-input
              v-model="inputValue"
              :type="inputType"
              @keydown.enter.native="handleInputEnter"
              :placeholder="inputPlaceholder"
              ref="input">
            </el-input>
             <!--检验错误的提示信息-->
            <div class="el-message-box__errormsg" :style="{ visibility: !!editorErrorMessage ? 'visible' : 'hidden' }">{{ editorErrorMessage }}</div>
          </div>
        </div>
        <!--弹框底部,包含:确认、取消按钮-->
        <div class="el-message-box__btns">
          <el-button
            :loading="cancelButtonLoading"
            :class="[ cancelButtonClasses ]"
            v-if="showCancelButton"
            :round="roundButton"
            size="small"
            @click.native="handleAction('cancel')"
            @keydown.enter="handleAction('cancel')">
            {{ cancelButtonText || t('el.messagebox.cancel') }}
          </el-button>
          <el-button
            :loading="confirmButtonLoading"
            ref="confirm"
            :class="[ confirmButtonClasses ]"
            v-show="showConfirmButton"
            :round="roundButton"
            size="small"
            @click.native="handleAction('confirm')"
            @keydown.enter="handleAction('confirm')">
            {{ confirmButtonText || t('el.messagebox.confirm') }}
          </el-button>
        </div>
      </div>
    </div>
  </transition>
</template>

<script type="text/babel">
此处的import在element按需导入的是采用了后编译的优化方法防止重复引入
  import Popup from 'element-ui/src/utils/popup';
  import Locale from 'element-ui/src/mixins/locale';
  import ElInput from 'element-ui/packages/input';
  import ElButton from 'element-ui/packages/button';
  import { addClass, removeClass } from 'element-ui/src/utils/dom';
  import { t } from 'element-ui/src/locale';
  import Dialog from 'element-ui/src/utils/aria-dialog';

  let messageBox;
  //定义的图标类型
  let typeMap = {
    success: 'success',
    info: 'info',
    warning: 'warning',
    error: 'error'
  };

  export default {
    mixins: [Popup, Locale],

    props: {
      modal: {
        default: true
      },
      lockScroll: { //是否在 MessageBox 出现时将 body 滚动锁定
        default: true
      },
      showClose: { //MessageBox 是否显示右上角关闭按钮
        type: Boolean,
        default: true
      },
      closeOnClickModal: { //是否可通过点击遮罩关闭 MessageBox
        default: true
      },
      closeOnPressEscape: { //是否可通过按下 ESC 键关闭 MessageBox
        default: true
      },
      closeOnHashChange: { //是否在 hashchange 时关闭 MessageBox
        default: true
      },
      center: { //是否居中布局
        default: false,
        type: Boolean
      },
      roundButton: { //是否使用圆角按钮
        default: false,
        type: Boolean
      }
    },

    components: {
      ElInput,
      ElButton
    },
<!-- 计算属性与button设置相同,都是全局参数改变后重新动态设置样式 -->
    computed: {
      icon() {
        const { type, iconClass } = this;
        //如果用户设置了自定义图标的类名,就显示自定义图标;如果没有就显示设置的type图标,否则就不显示图标
        return iconClass || (type && typeMap[type] ? `el-icon-${ typeMap[type] }` : '');
      },
      //添加确定按钮的自定义类名
      confirmButtonClasses() {
        return `el-button--primary ${ this.confirmButtonClass }`;
      },
      //添加取消按钮的自定义类名
      cancelButtonClasses() {
        return `${ this.cancelButtonClass }`;
      }
    },

    methods: {
      getSafeClose() {
        const currentId = this.uid;
        return () => {
          this.$nextTick(() => {
            if (currentId === this.uid) this.doClose();
          });
        };
      },
      doClose() {
        if (!this.visible) return;
        this.visible = false;
        this._closing = true;

        this.onClose && this.onClose();
        messageBox.closeDialog(); // 解绑
        if (this.lockScroll) {
          setTimeout(this.restoreBodyStyle, 200);
        }
        this.opened = false;
        this.doAfterClose();
        setTimeout(() => {
          if (this.action) this.callback(this.action, this);
        });
      },
      //点击弹框时,根据closeOnClickModal来是否可通过点击遮罩关闭 MessageBox
      handleWrapperClick() {
        // 如果closeOnClickModal设置为true
        if (this.closeOnClickModal) {
          //判断是否将取消(点击取消按钮)与关闭(点击关闭按钮或遮罩层、按下 ESC 键)进行区分
          //如果区分则this.handleAction('close');否则this.handleAction('cancel');
          this.handleAction(this.distinguishCancelAndClose ? 'close' : 'cancel');
        }
      },

      handleInputEnter() {
        if (this.inputType !== 'textarea') {
          return this.handleAction('confirm');
        }
      },

      handleAction(action) {
        // 如果当前是this.$prompt
        if (this.$type === 'prompt' && action === 'confirm' && !this.validate()) {
          return;
        }
        this.action = action;
        //判断beforeClose是否是函数,也就是用户是否定义了beforeClose函数
        if (typeof this.beforeClose === 'function') {
          this.close = this.getSafeClose();
          this.beforeClose(action, this, this.close);
        } else {
          //如果用户没有定义beforeClose,就调doClose直接关闭弹框
          this.doClose();
        }
      },
      //该方法主要是用于用户在调用$prompt方法打开消息提示时,校验input输入框的值
      validate() {
        //$prompt方法即可打开消息提示,它模拟了系统的 prompt
        if (this.$type === 'prompt') {
          //获取用户自己定义的匹配模式
          const inputPattern = this.inputPattern;
          //如果用户自己定义了匹配模式,并且用户输入校验不通过
          if (inputPattern && !inputPattern.test(this.inputValue || '')) {
            //显示用户自己定义的校验不通过时的提示信息;当用户未定义校验不通过的提示信息时,t('el.messagebox.error')输出提示:输入的数据不合法!
            this.editorErrorMessage = this.inputErrorMessage || t('el.messagebox.error');
            //这里主要是在校验不通过时,给input加上的类名中加上invalid,变成class="el-input__inner invalid"
            //通过.el-message-box__input input.invalid{border-color: #f56c6c;}改变input的border为红色
            addClass(this.getInputElement(), 'invalid');
            return false;
          }
          //输入框的校验函数;可以返回布尔值或字符串,若返回一个字符串, 则返回结果会被赋值给 inputErrorMessage
          const inputValidator = this.inputValidator;
          //如果校验函数存在
          if (typeof inputValidator === 'function') {
            const validateResult = inputValidator(this.inputValue);
            //校验不通过,显示校验不通过的红色提示信息
            if (validateResult === false) {
              this.editorErrorMessage = this.inputErrorMessage || t('el.messagebox.error');
              addClass(this.getInputElement(), 'invalid');
              return false;
            }
            //若返回一个字符串, 则返回结果会被赋值给 inputErrorMessage
            if (typeof validateResult === 'string') {
              this.editorErrorMessage = validateResult;
              addClass(this.getInputElement(), 'invalid');
              return false;
            }
          }
        }
        //如果校验通过,则不显示错误提示,并删除类名invalid
        this.editorErrorMessage = '';
        removeClass(this.getInputElement(), 'invalid');
        return true;
      },
      getFirstFocus() {
        const btn = this.$el.querySelector('.el-message-box__btns .el-button');
        const title = this.$el.querySelector('.el-message-box__btns .el-message-box__title');
        return btn || title;
      },
      //获取input元素
      getInputElement() {
        const inputRefs = this.$refs.input.$refs;
        return inputRefs.input || inputRefs.textarea;
      }
    },

    watch: {
      inputValue: {
        immediate: true,
        handler(val) {
          this.$nextTick(_ => {
            if (this.$type === 'prompt' && val !== null) {
              this.validate();
            }
          });
        }
      },

      visible(val) {
        if (val) {
          this.uid++;
          if (this.$type === 'alert' || this.$type === 'confirm') {
            this.$nextTick(() => {
              this.$refs.confirm.$el.focus();
            });
          }
          this.focusAfterClosed = document.activeElement;
          messageBox = new Dialog(this.$el, this.focusAfterClosed, this.getFirstFocus());
        }

        // prompt
        if (this.$type !== 'prompt') return;
        if (val) {
          setTimeout(() => {
            if (this.$refs.input && this.$refs.input.$el) {
              this.getInputElement().focus();
            }
          }, 500);
        } else {
          this.editorErrorMessage = '';
          removeClass(this.getInputElement(), 'invalid');
        }
      }
    },

    mounted() {
      this.$nextTick(() => {
        //根据设置的closeOnHashChange参数判断是否在 hashchange 时关闭 MessageBox
        if (this.closeOnHashChange) {
          //如果需要,则在元素挂载之后,给window添加hashchange事件
          window.addEventListener('hashchange', this.close);
        }
      });
    },

    beforeDestroy() {
      // 组件销毁时移除hashchange事件
      if (this.closeOnHashChange) {
        window.removeEventListener('hashchange', this.close);
      }
      setTimeout(() => {
        messageBox.closeDialog();
      });
    },

    data() {
      return {
        uid: 1,
        title: undefined, //MessageBox 标题
        message: '',  //MessageBox 消息正文内容
        type: '', //消息类型,用于显示图标
        iconClass: '', //自定义图标的类名,会覆盖 type
        customClass: '',
        showInput: false,  //是否显示输入框
        inputValue: null, //输入框的初始文本
        inputPlaceholder: '', //输入框的占位符
        inputType: 'text', //输入框的类型
        inputPattern: null,//输入框的校验表达式
        inputValidator: null, //输入框的校验函数。可以返回布尔值或字符串,若返回一个字符串, 则返回结果会被赋值给 inputErrorMessage
        inputErrorMessage: '', //校验未通过时的提示文本
        showConfirmButton: true, //是否显示确定按钮
        showCancelButton: false, //是否显示取消按钮
        action: '',
        confirmButtonText: '', //确定按钮的文本内容
        cancelButtonText: '', //取消按钮的文本内容
        confirmButtonLoading: false,
        cancelButtonLoading: false,
        confirmButtonClass: '',  //确定按钮的自定义类名
        confirmButtonDisabled: false,
        cancelButtonClass: '', //取消按钮的自定义类名
        editorErrorMessage: null,
        callback: null,
        dangerouslyUseHTMLString: false, //是否将取消(点击取消按钮)与关闭(点击关闭按钮或遮罩层、按下 ESC 键)进行区分
        focusAfterClosed: null,
        isOnComposition: false,
        distinguishCancelAndClose: false //是否将取消(点击取消按钮)与关闭(点击关闭按钮或遮罩层、按下 ESC 键)进行区分
      };
    }
  };
</script>

const defaults = {
  title: null, //MessageBox 标题
  message: '', //MessageBox 消息正文内容
  type: '', //消息类型,用于显示图标
  iconClass: '', //自定义图标的类名,会覆盖 type
  showInput: false, //是否显示输入框
  showClose: true, //MessageBox 是否显示右上角关闭按钮
  modalFade: true,
  lockScroll: true,//是否在 MessageBox 出现时将 body 滚动锁定
  closeOnClickModal: true, //是否可通过点击遮罩关闭 MessageBox
  closeOnPressEscape: true, //是否可通过按下 ESC 键关闭 MessageBox
  closeOnHashChange: true, //是否在 hashchange 时关闭 MessageBox
  inputValue: null, //输入框的初始文本
  inputPlaceholder: '', //输入框的占位符
  inputType: 'text', //输入框的类型
  inputPattern: null, //输入框的校验表达式
  inputValidator: null, //输入框的校验函数。可以返回布尔值或字符串,若返回一个字符串, 则返回结果会被赋值给 inputErrorMessage
  inputErrorMessage: '', //校验未通过时的提示文本
  showConfirmButton: true, //是否显示确定按钮
  showCancelButton: false, //是否显示取消按钮
  confirmButtonPosition: 'right', //
  confirmButtonHighlight: false,
  cancelButtonHighlight: false,
  confirmButtonText: '', //确定按钮的文本内容
  cancelButtonText: '', //取消按钮的文本内容
  confirmButtonClass: '', //确定按钮的自定义类名
  cancelButtonClass: '', //取消按钮的自定义类名
  customClass: '', //MessageBox 的自定义类名
  beforeClose: null, //MessageBox 关闭前的回调,会暂停实例的关闭
  dangerouslyUseHTMLString: false, //是否将取消(点击取消按钮)与关闭(点击关闭按钮或遮罩层、按下 ESC 键)进行区分
  center: false, //是否居中布局
  roundButton: false, //是否使用圆角按钮
  distinguishCancelAndClose: false //是否将取消(点击取消按钮)与关闭(点击关闭按钮或遮罩层、按下 ESC 键)进行区分
};

import Vue from 'vue';
import msgboxVue from './main.vue';
 // 合并对象的工具函数 
import merge from 'element-ui/src/utils/merge';
import { isVNode } from 'element-ui/src/utils/vdom';

//创建MessageBox的构造器,包含msgboxVue组件选项的对象作为Vue.extend的参数,返回一个VueComponent类,VueComponent类是Vue类的子类
//Vue.extend是一个类构造器,用来创建一个子类vue并返回构造函数,而Vue.component它的任务是将给定的构造函数与字符串ID相关联,以便Vue.js可以在模板中接收它。
const MessageBoxConstructor = Vue.extend(msgboxVue);

let currentMsg, instance;
let msgQueue = [];
// defaultCallback处理了两种回调方式 :手动传入callback函数和默认的promise
const defaultCallback = action => {
  if (currentMsg) {
    let callback = currentMsg.callback;
    if (typeof callback === 'function') {
      if (instance.showInput) {
        callback(instance.inputValue, action);
      } else {
        callback(action);
      }
    }
  // 处理promise
    if (currentMsg.resolve) {
      // 点击确定或者去下关闭按钮时,在此处调对应的方法进行处理
      if (action === 'confirm') { //执行确认后的回调方法
        if (instance.showInput) {
          currentMsg.resolve({ value: instance.inputValue, action });
        } else {
          currentMsg.resolve(action);
        }
      } else if (currentMsg.reject && (action === 'cancel' || action === 'close')) {  //执行取消和关闭后的回调方法
        currentMsg.reject(action);
      }
    }
  }
};
// 创建一个新的vue子实例
const initInstance = () => {
  //instance为messageBox的实例
  instance = new MessageBoxConstructor({
    el: document.createElement('div')
  });
// 给实例添加callback 对象
  instance.callback = defaultCallback;
};

const showNextMsg = () => {
  if (!instance) {
    // 调用initInstance初始化实例,返回messageBox的实例对象
    initInstance();
  }
  instance.action = '';

  if (!instance.visible || instance.closeTimer) {
    if (msgQueue.length > 0) {
      currentMsg = msgQueue.shift();

      let options = currentMsg.options;
      //将用户设置的属性和方法挂载到messageBox的实例对象instance上去
      for (let prop in options) {
        if (options.hasOwnProperty(prop)) {
          instance[prop] = options[prop];
        }
      }
      //当用户未设置callback时,将defaultCallback赋值给instance.callback
      if (options.callback === undefined) {
         // 当options里面有callback传入,正常输出。
        // 当options里面没有callback,instance.callback使用defaultCallback
        instance.callback = defaultCallback;
      }
// 再次封装callback
      let oldCb = instance.callback;
      instance.callback = (action, instance) => {
        oldCb(action, instance);
        showNextMsg();
      };
        // 判断message是否传入的是Html片段,如果是Html片段添加到slot
      if (isVNode(instance.message)) {
        instance.$slots.default = [instance.message];
        instance.message = null;
      } else {
        delete instance.$slots.default;
      }
          // 将特定的参数设定初始值
      ['modal', 'showClose', 'closeOnClickModal', 'closeOnPressEscape', 'closeOnHashChange'].forEach(prop => {
        if (instance[prop] === undefined) {
          instance[prop] = true;
        }
      });
        // 注意message是挂载到body上
      document.body.appendChild(instance.$el);
  // 控制弹窗出现
      Vue.nextTick(() => {
        instance.visible = true;
      });
    }
  }
};

const MessageBox = function(options, callback) {
  if (Vue.prototype.$isServer) return;
  if (typeof options === 'string' || isVNode(options)) {
       // 当options参数为字符串 this.$msgbox('xxx')情况下默认设置message字段
    options = {
      message: options
    };
     // 若有两个及以上参数判断第二个参数是否为字符串赋值给title
    if (typeof arguments[1] === 'string') {
      options.title = arguments[1];
    }
  } else if (options.callback && !callback) {
     // 参数为对象且对象有callback字段时 将callback赋值给callback
    callback = options.callback;
  }
 // 兼容不支持Promise情况
  if (typeof Promise !== 'undefined') {
    return new Promise((resolve, reject) => { // eslint-disable-line
      // options合并默认的所有参数和用户设置的参数
      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();
  }
};

MessageBox.setDefaults = defaults => {
  MessageBox.defaults = defaults;
};
//this.$alert方法
MessageBox.alert = (message, title, options) => {
  //如果title的类型为object时,用户可能没传title,则将title的值赋值给options
  if (typeof title === 'object') {
    options = title;
    title = '';
  } else if (title === undefined) {
    title = '';
  }
  //合并用户传的参数,并调用MessageBox方法
  return MessageBox(merge({
    title: title,
    message: message,
    $type: 'alert',
    closeOnPressEscape: false,
    closeOnClickModal: false
  }, options));
};
//this.$confirm方法,分析同MessageBox.alert方法
MessageBox.confirm = (message, title, options) => {
  if (typeof title === 'object') {
    options = title;
    title = '';
  } else if (title === undefined) {
    title = '';
  }

  return MessageBox(merge({
    title: title,
    message: message,
    $type: 'confirm',
    showCancelButton: true
  }, options));
};
//this.$prompt方法,分析同MessageBox.alert方法
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));
};

MessageBox.close = () => {
  instance.doClose();
  instance.visible = false;
  msgQueue = [];
  currentMsg = null;
};

export default MessageBox;
export { MessageBox };


几个需要注意的点:
1 在main.vue中import了很多非MessageBox的组件,而我们知道element-Ui是可以按需引入的,如果每一次按需引入不同的包是不是都会重复导入这些文件,源码是怎么处理的呢?
image.png
这里先给出结论:
依赖包提供源码,而编译交给应用处理,这一不仅不会有组件冗余代码,甚至连编译的冗余代码都不会有,使用了后编译的思想,
具体参考这篇文章:链接

三 功能详细分析

1 消息提示

前面提到 msgBox本质上就是MessageBox我们在开发中使用到的msgBox本质上就是MessageBox 我们在开发中使用到的alert,confirm,confirm,prompt三个调出弹窗组件的方法也是对MessageBox的再封装:
先看官网是如何使用MessageBox的:

<template>
  <el-button type="text" @click="open">点击打开 Message Box</el-button>
</template>

<script>
  export default {
    methods: {
      open() {
        this.$alert('这是一段内容', '标题名称', {
          confirmButtonText: '确定',
          callback: action => {
            this.$message({
              type: 'info',
              message: `action: ${ action }`
            });
          }
        });
      }
    }
  }
</script>

在open函数中使用了this.$alert方法对应的是message-box下的main.js中的MessageBox.alert方法如下:

//this.$alert方法
MessageBox.alert = (message, title, options) => {
  //如果title的类型为object时,用户可能没传title,则将title的值赋值给options
  if (typeof title === 'object') {
    options = title;
    title = '';
  } else if (title === undefined) {
    title = '';
  }
  //合并用户传的参数,并调用MessageBox方法
  return MessageBox(merge({
    title: title,
    message: message,
    $type: 'alert',
    closeOnPressEscape: false,
    closeOnClickModal: false
  }, options));
};

注:merge为合并对象函数,在上述源码中有提到
为什么是MessageBox.alert方法呢,查找后得知:在elementUI的src/index.js中:(这里也解释了上面说的;msgBox本质上就是MessageBox我们在开发中使用到的msgBox本质上就是MessageBox 我们在开发中使用到的alert,confirm,confirm,prompt三个调出弹窗组件的方法也是对MessageBox的再封装:)

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

这也是我们在开发中**使用message弹窗时不需要像button一样写结构样式的原因(进行了$alert的二次封装),**实现了1 消息提示功能。

2 确认/取消功能

官网示例中:传递了confirmButtonText和cancelButtonText;对比普通的消息提示多了一个 .then 和catch来捕获this.confirm返回的结果。

<template>
  <el-button type="text" @click="open">点击打开 Message Box</el-button>
</template>

<script>
  export default {
    methods: {
      open() {
        this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          this.$message({
            type: 'success',
            message: '删除成功!'
          });
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消删除'
          });          
        });
      }
    }
  }
</script>

通过函数调用的方式生成弹窗后,如果支持promie,当点击确定的时候,执行.then方法,当点击“取消”按钮或右上角关闭按钮时,执行catch方法;或者通过传入callback回调函数的方式,来处理“确定”、“取消”和“关闭”等。
来到源码中看:MessageBox/main.js

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) => { // eslint-disable-line
      // options合并默认的所有参数和用户设置的参数
      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();
  }
};

这个函数对message和callback做了处理,
msqQueue(前面定义的,后面实际上返回给了options)作为一个数组保存了options和callback,其中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 });
        } else {
          currentMsg.resolve(action);
        }
      } else if (currentMsg.reject && (action === 'cancel' || action === 'close')) {  //执行取消和关闭后的回调方法
        currentMsg.reject(action);// 如果支持promise,当action为'cancel'或'close'时,调用下一步的catch方法
      }
    }
  }
};

3 提交内容

关键在于分析:如何显示input以及获取input的值。
调用$prompt方法即可打开消息提示,它模拟了系统的 prompt。可以用inputPattern字段自己规定匹配模式,或者用inputValidator规定校验函数,可以返回Boolean或String,返回false或字符串时均表示校验未通过,同时返回的字符串相当于定义了inputErrorMessage字段。此外,可以用inputPlaceholder字段来定义输入框的占位符。
开发代码实现:

<template>
  <el-button type="text" @click="open">点击打开 Message Box</el-button>
</template>

<script>
  export default {
    methods: {
      open() {
        this.$prompt('请输入邮箱', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          inputPattern: /[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?/,
          inputErrorMessage: '邮箱格式不正确'
        }).then(({ value }) => {
          this.$message({
            type: 'success',
            message: '你的邮箱是: ' + value
          });
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '取消输入'
          });       
        });
      }
    }
  }
</script>

源码分析:

//this.$prompt方法,分析同MessageBox.alert方法
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));
};

这里的关键是:显示input输入框

showInput: true,
    $type: 'prompt'

在defaultCallback函数中:
当instance.showInput为true时候 会callback会他的值inputValue和action 否则的话只返回action 还有取消和确认的方法,这样看就比较清晰的能看到如何获取值了,这里的inputValue早开发时作为then的参数value传回,能直接使用

const defaultCallback = action => {
  if (currentMsg) {
    let callback = currentMsg.callback;
    if (typeof callback === 'function') {
      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 });
        } else {
          currentMsg.resolve(action);
        }
      } else if (currentMsg.reject && (action === 'cancel' || action === 'close')) {  //执行取消和关闭后的回调方法
        currentMsg.reject(action);
      }
    }
  }
};

4 自定义内容

以上三个方法都是对msgbox方法的再包装。本例直接调用msgbox方法的再包装。本例直接调用msgbox方法,使用了showCancelButton字段,用于显示取消按钮。另外可使用cancelButtonClass为其添加自定义样式,使用cancelButtonText来自定义按钮文本(Confirm 按钮也具有相同的字段,在文末的字段说明中有完整的字段列表)。此例还使用了beforeClose属性,它的值是一个方法,会在 MessageBox 的实例关闭前被调用,同时暂停实例的关闭。它有三个参数:action、实例本身和done方法。使用它能够在关闭前对实例进行一些操作,比如为确定按钮添加loading状态等;此时若需要关闭实例,可以调用done方法(若在beforeClose中没有调用done,则实例不会关闭)——官网解说
这里官网的描述已经很清晰了,就不在赘述。
开发使用:

<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>

源码分析:
全局搜索$msgbox 后发现:

  Vue.prototype.$msgbox = MessageBox;

也就是直接对MessageBox的应用
这里传入的参数分别为:title;message;showCancelButton;confirmButtonText;cancelButtonText;beforeClose。重点分析message和beforeClose。
案例中message采用一**个render函数。**创建了一个文本p节点。(这点有点不太确定,后面在来补,vue选手落泪!,问了一下群里的大佬有说jsx,也有说render函数的)
beforeClose: null, //MessageBox 在源码中的解释为 关闭前的回调,会暂停实例的关闭。如果是确认在关闭前会经过定时器里面的逻辑。

5 使用HTML片段

直接在$alert中的第一个参数中传入html为参数,在源码中没有找到对message做特殊处理的地方

6 区分取消与关闭

默认情况下,当用户触发取消(点击取消按钮)和触发关闭(点击关闭按钮或遮罩层、按下 ESC 键)时,Promise 的 reject 回调和callback回调的参数均为 'cancel'。如果将distinguishCancelAndClose属性设置为 true,则上述两种行为的参数分别为 'cancel' 和 'close'。

居中样式

组件最外层有一类名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;
}


跟上图中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;
}

也是写了很久才写完,在学习源码的过程中也发现自己的诸多不足,文章也又很多地方没有研究明白,只能当做学习笔记记录一下,后面学习到之后再来补全。
参考链接:element-ui源码笔记整理 参考链接