elementUI——MessageBox组件源码分析

说明:以下基于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;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349