Element组件MessageBox剖析

因最近业务需求需要实现类似于Element中的MessageBox组件的效果,所以尝试封装了一个类似的小组件,本文不介绍封装,因为受到MessageBox的启发,所以通过源码注释的方式详细剖析一下Element的MessageBox实现思想。

基础知识

Vue.extend(options)

  • 参数:{Object} options
  • 用法:使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。需要注意的是:data 选项是特例,需要注意 - 在 Vue.extend() 中它必须是函数
1
<div id="mount-point"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建构造器
var Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data: function () {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')

结果:

1
<p>Walter White aka Heisenberg</p>

为什么要介绍Vue.extend(options),因为Element中MessageBox的使用方式为函数式调用 (this.$confirm())的形式,以这种形式调用组件就不能按照常规的组件引入注册的形式去调用,可以通过Vue.extend(options)创建一个vue子类再通过函数暴露出去的方式实现函数式调用。

Element MessageBox 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
// main.js 仅介绍主要部分代码
...

import Vue from 'vue';
// 引入模板文件
import msgboxVue from './main.vue';
// element合并对象的工具函数,代码比较简单可以自行查阅
import merge from 'element-ui/src/utils/merge';
import { isVNode } from 'element-ui/src/utils/vdom';

// 创建一个新的vue构造器,构造器可以手动挂载到一个新的Dom上
const MessageBoxConstructor = Vue.extend(msgboxVue);

let currentMsg, instance;
let msgQueue = [];

// 创建一个新的vue子实例
const initInstance = () => {
instance = new MessageBoxConstructor({
el: document.createElement('div')
});
// 给实例添加callback对象,后面会分析到
instance.callback = defaultCallback;
};

// defaultCallback处理了两种形式的回调方式
// 1.可以手动传入一个callback函数
// 2.使用默认的promise方式
const defaultCallback = action => {
if (currentMsg) {
let callback = currentMsg.callback;
// 处理传入回调函数情况
if (typeof callback === 'function') {
// showInput区分是否为输入框MessageBox
if (instance.showInput) {
callback(instance.inputValue, action);
} else {
callback(action, instance);
}
}
// 处理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);
}
}
}
};

const showNextMsg = () => {
if (!instance) {
initInstance();
}
instance.action = '';

if (!instance.visible || instance.closeTimer) {
if (msgQueue.length > 0) {
// 顺序执行msgQueue中的currentMsg
currentMsg = msgQueue.shift();
// currentMsg内容如下:
// {
// options: merge({}, defaults, MessageBox.defaults, options),
// callback: callback,
// resolve: resolve,
// reject: reject
// }
let options = currentMsg.options;
// 将参数挂载到新创建的实例data上
for (let prop in options) {
if (options.hasOwnProperty(prop)) {
// 实例参数修改
instance[prop] = options[prop];
}
}

// 如果options没传入callback将默认的callback赋值给实例的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
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();
}
};

// 使用方式1:
// this.$msgbox({title:'测试',message:'测试',callback:(action,instance)=>{
// console.log(action) //confirm
// console.log(instance) //vue实例
// }})
// 使用方式2:
// this.$msgbox({title:'测试',message:'测试'},(action,instance)=>{
// console.log(action) //confirm
// console.log(instance) //undefined
// })

...

// 暴露MessageBox方法

export default MessageBox;
export { MessageBox };

可以看出Message组件主要的两个方法一个是MessageBox,一个是showNextMsg,这两个方法的主要功能一个是添加新的message对象一个是设置实例参数,除了callback的理解有些复杂外其他的代码理解应该不难。至于main.vue文件就是普通的vue文件不再赘述。

简单实现

MessageBox组件
源码