说明:以下基于elementUI@2.13.1。
从场景上说,MessageBox 的作用是美化系统自带的 alert、confirm 和 prompt,因此适合展示较为简单的内容。如果需要弹出较为复杂的内容,请使用 Dialog。
本次主要分析MessageBox
以及基于MessageBox
的alert
、confirm
和 prompt
。
阅读以下内容的前提是对官网示例和组件用法有了基本了解。
在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
(取消+确定)
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;
}
- 跟上图中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中watchvisible
,当值为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),通过classel-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中弹框右上角关闭按钮点击事件
流程走到最后会执行
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. 对message
和callback
做处理
b. msgQueue
数组保存options和callback,如果浏览器支持Promise
,那么再将resovle
和reject
封装进msgQueue
,分别触发后续then
和catch
逻辑,可见下面的官方示例:
// 官方示例
<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>
其中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;