element 源码学习五 —— Notice 系列组件学习

消息提示行为是开发中非常常见的功能,Element 为我们提供了非常好用和美观的消息提示组件。这里就简单学习下 Notice 组件的 CSS 和代码逻辑。

简介

Notice 包括了五类组件:

  • Alert 用于页面中展示重要的提示信息。
  • Loading 加载数据时显示动效。
  • Message 常用于主动操作后的反馈提示。与 Notification 的区别是后者更多用于系统级通知的被动提醒。
  • MessageBox 模拟系统的消息提示框而实现的一套模态对话框组件,用于消息提示、确认消息和提交内容。
  • Notification 悬浮出现在页面角落,显示全局的通知提醒消息。

本文中不同角度来学习这些组件(这些组件有很多相似性,所以一起学习啦~)。

demo

下面是参照 element ui 写的一个小demo,尝试着了解下其中的 CSS
贴代码:

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>My Notice</title>
<style>
.success-color {
background-color: #f0f9eb;
color: #67c23a;
}

.info-color {
background-color: #f4f4f5;
color: #909399;
}

.warning-color {
background-color: #fdf6ec;
color: #e6a23c;
}

.error-color {
background-color: #fef0f0;
color: #f56c6c;
}


.alert-container {
position: relative;
padding: 8px 16px;
border-radius: 5px;
opacity: 1;
align-items: center;
overflow: hidden;
display: flex;
}

.success-text {
font-size: 13px;
line-height: 18px;
padding: 0 8px;
color: #67c23a;
}

.close {
font-size: 13px;
position: absolute;
top: 12px;
right: 15px;
cursor: pointer;
}

.loading-background {
position: fixed;
z-index: 2000;
background-color: rgba(26, 26, 26, 0.9);
margin: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
transition: opacity 0.3s;
z-index: 2000;
}

.loading-div {
top: 50%;
position: absolute;
margin-top: -20px;
height: 40px;
width: 100%;
align-items: center;
justify-content: center;
display: flex;

}

.loading-text {
color: #f0f9eb;
font-size: 15px;
overflow: hidden;
z-index: 2001;
}

.el-message {
min-width: 380px;
box-sizing: border-box;
border-radius: 4px;
border-width: 1px;
border-style: solid;
border-color: #ebeef5;
position: fixed;
left: 50%;
top: 20px;
transform: translateX(-50%);
background-color: #edf2fc;
transition: opacity 0.3s, transform 0.4s;
overflow: hidden;
padding: 15px 15px 15px 20px;
display: flex;
align-items: center;
}
</style>
</head>

<body>
<div id="app">
<div class="alert-container success-color">
<span class="success-text">成功提示的文案</span>
<span class="close">X</span>
</div>
<button id="showLoading">显示加载中</button>
<button id="showMessage">显示信息</button>
<div class="el-message" id="message" style="display:none;">
<p>这是一条消息</p>
</div>
</div>

<script>
var alertContainer = document.getElementsByClassName("alert-container")[0]
var close = document.getElementsByClassName("close")[0]
close.onclick = function () {
alertContainer.style = "display:none;"
}

var app = document.getElementById("app")
var bg = document.createElement("div")
bg.className = "loading-background"
var div = document.createElement("div")
div.className = "loading-div"
var text = document.createElement("span")
text.className = "loading-text"
text.textContent = "加载中……"
div.appendChild(text)
bg.appendChild(div)


document.getElementById("showLoading").onclick = function () {
if (document.getElementsByClassName("loading-background").length > 0) {
return;
}
app.appendChild(bg)
setTimeout(() => {
if (document.getElementsByClassName("loading-background").length > 0) {
app.removeChild(bg)
}
}, 3000)
}

document.getElementById("showMessage").onclick = function () {
document.getElementById("message").style = "display:block;"
setTimeout(() => {
document.getElementById("message").style = "display:none;"
}, 3000)
}
</script>
</body>

</html>

代码的运行结果请看 ->这里<-

以上代码实现了

  • Alert 的样式
  • loading 的简单版本
  • message 的无动画版本。

只实现这三个功能的原因是另外两个功能和扩展功能都是基于这个demo扩展的。
Alert 是一个静态的文本内容,只是外部包裹了带样式的容器。可能再多个隐藏按钮和消息图标。
Loading 和 MessageBox 其实的基本逻辑是插入或显示新的内容,并在显示完成后消失。需要注意的就是后面要添加一层遮罩阴影,遮罩如果非全屏使用 position:absolute 而全屏则使用 position:fixed 覆盖。另外就是注意 z-index 属性将组件放到视图最上层。
Message 和 Notification 其实就是文本内容、图标和按钮组合容器的现实和隐藏过程。它们的过渡动画使用的是 vue 的进入/离开 & 列表过渡来实现。

通过几个问题来看源码

其实从样式上,上面的 demo 已经实现了大致的样子了。下面来看看组件的一些逻辑。源码内容比较多,所以就以问答的方式有目的的来看源码。

Alert 的界面实现?

Alert 由图标、文本内容、描述内容和关闭按钮组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<transition name="el-alert-fade">
<div
class="el-alert"
:class="[typeClass, center ? 'is-center' : '']"
v-show="visible"
role="alert"
>
<!-- 图标 -->
<i class="el-alert__icon" :class="[ iconClass, isBigIcon ]" v-if="showIcon"></i>
<div class="el-alert__content">
<!-- 标题 -->
<span class="el-alert__title" :class="[ isBoldTitle ]" v-if="title">{{ title }}</span>
<slot>
<!-- 插槽,默认插入描述文本 -->
<p class="el-alert__description" v-if="description">{{ description }}</p>
</slot>
<!-- 关闭图标按钮 -->
<i class="el-alert__closebtn" :class="{ 'is-customed': closeText !== '', 'el-icon-close': closeText === '' }" v-show="closable" @click="close()">{{closeText}}</i>
</div>
</div>
</transition>

组件很简单,注释上都写了~组件还做了 slot 插槽拓展,可以在 <el-alert> 标签内插入自定义内容。

如何显示纯文本、HTML 和 VNode

显示文本使用 指令来显示内容。
显示HTML使用 v-html 指令来渲染显示。

1
2
3
4
<!-- 显示文本 -->
<p v-if="!dangerouslyUseHTMLString" class="el-message__content">{{ message }}</p>
<!-- 显示HTML -->
<p v-else v-html="message" class="el-message__content"></p>

使用 vue 的 render 函数生成 VNode 对象传给组件作为组件 slot 插槽的默认显示结果。

1
2
3
4
if (isVNode(instance.message)) {
instance.$slots.default = [instance.message];
instance.message = null;
}

组件的过渡动画怎么来的

使用 Notice 系列组件时,发现组件的显示和消失都是有过渡动画的。界面看着更加友好和舒服。实现方式就是使用了 vue 的进入/离开 & 列表过渡来实现效果。

界面的关闭使用的是什么方式?

对于 loading 和 message-box ,在没有界面时会在 body 最后添加组件内容,显示过后使用 v-show="false"(display:none;) 隐藏,随后就调用显示和隐藏界面。

1
2
// 将 message-box 组件加入到 body 中
document.body.appendChild(instance.$el);

对于 Alert 由于一开始就显示,只是删除按钮,所以只需修改 v-show 属性隐藏即可。
对于 message 和 notification,这两个组件可以多次弹出,逐个关闭(自动或手动)。所以这两个组件是保存在一个数组中,然后进行渲染的,关闭某个组件就是一个将组件从组件数组中移除的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// id 组件id
// useOnClose 自定义关闭函数
Message.close = function(id, userOnClose) {
for (let i = 0, len = instances.length; i < len; i++) {
if (id === instances[i].id) {
// 找到组件,执行自定义关闭函数并从数组中移除
if (typeof userOnClose === 'function') {
userOnClose(instances[i]);
}
instances.splice(i, 1);
break;
}
}
};

message-box 的回调函数实现

对于 message-box 有两种函数回调:callback 函数和 Promise 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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();
}

当然,到这一步只是将组件配置和回调函数组成对象,并没有执行。执行实在 showNextMsg 方法中。showNextMsg 方法用于组合当前组件 options 并写入到 DOM 中,然后显示组件。其中有这么一段关于回调的:

1
2
3
4
// 如果 currentMsg.options.callback 为 undefined
if (options.callback === undefined) {
instance.callback = defaultCallback;
}

所以再看看 defaultCallback 函数对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 (action === 'cancel' && currentMsg.reject) {
currentMsg.reject(action);
}
}
}
};

这里就可以看到回调函数和 Promise 的调用和传参过程。当组件“关闭”的时候执行 callback 方法回调:

1
2
3
4
5
6
doClose() {
……
setTimeout(() => {
if (this.action) this.callback(this.action, this);
});
},

至此实现了回调及其传参。

如何实现显示多个 Notification 向下偏移插入?

偏移量计算在 Notification 的构造方法中计算获得当前组件 verticalOffset

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
const Notification = function(options) {
// 服务器渲染
if (Vue.prototype.$isServer) return;
options = options || {};
const userOnClose = options.onClose; // 自定义关闭
const id = 'notification_' + seed++; // 组件 id
const position = options.position || 'top-right'; // 位置
// 关闭事件
options.onClose = function() {
Notification.close(id, userOnClose);
};
// 组件实例
instance = new NotificationConstructor({
data: options
});
// vnode
if (isVNode(options.message)) {
instance.$slots.default = [options.message];
options.message = 'REPLACED_BY_VNODE';
}
instance.id = id;
instance.vm = instance.$mount();
// 添加实例
document.body.appendChild(instance.vm.$el);
instance.vm.visible = true;
instance.dom = instance.vm.$el;
instance.dom.style.zIndex = PopupManager.nextZIndex();
// 偏移量计算
let verticalOffset = options.offset || 0;
instances.filter(item => item.position === position).forEach(item => {
verticalOffset += item.$el.offsetHeight + 16;
});
verticalOffset += 16;
instance.verticalOffset = verticalOffset;
// 传入数组
instances.push(instance);
return instance.vm;
};

偏移量 verticalOffset 在组件 package\notification/src/main.vue 中使用。

1
<div :style="positionStyle" ></div>

1
2
3
4
5
6
7
8
9
10
11
12
computed: {
// 正则匹配 position 是 top 还是 bottom
verticalProperty() {
return /^top-/.test(this.position) ? 'top' : 'bottom';
},
// 最后返回的内联样式
positionStyle() {
return {
[this.verticalProperty]: `${ this.verticalOffset }px`
};
}
}

如果 positionbottom-left 偏移量 verticalOffset 为 20,那么返回的内联样式就是:

1
2
3
{
bottom: 20px;
}

至此实现了偏移的功能。

最后

这里简单学习了一下 Notice 系列组件的样式和逻辑。从中学到了:

  • 用 CSS 设计样式和动态修改样式属性。
  • 用了许多 DOM 的操作
  • 使用已有轮子 —— 用到了 Vue 的 directive、 transition 和 render。
  • 组件设计方面,学到了使用数组管理多个同类组件;使用 <slot> 标签给开发者预留拓展空间,使用构造函数的方法来拓展组件逻辑并定义一些快捷方法。

个人感觉学习一些成熟组件的源码能够学到不少东西,收获不少。