『element-notification』组件解析

element 的 notification 消息组件解析

5Kq81.png
题图无关

这是承继上一篇——不知道哪一篇提到我在公司内部写一套 ui 控件的下一篇。因为最近也需要实现一个类似 element 的 notification 消息组件。
因此便直接研究参考了 element 的实现源码,相对简单,于是写一下记下来。

基本实现原理

总体来说这个组件还是比较简单的。我对比了一下 iview 跟 element 的实现,个人感觉 element
的更好。

组件结构

先来看看这个组件的目录结构。

notification
├── index.js
├── src
| ├── main.js
| └── main.vue

除开统一的暴露文件 index.js, 主要的就是 main.jsmain.vue (明明也就三个文件……

  • main.vue 消息组件的主体,控制消息组件的具体结构样式与组件逻辑
  • main.js 基于上面的组件主体,构造命令式的新增/关闭消息组件逻辑,以及通过一个数组,记录所有的消息组件实例,以计算控制消息组件的具体定位

关键点解析

根据组件的目录结构,我们已经可以看出整个组件逻辑了。消息组件本身的结构没什么好说,
主要有意思的地方是 main.js

构造命令式调用

首先是为了构造命令式的调用方式, 在 main.js 中通过 vue.extend 以 main.vue 参数生成一个构造函数 NotificationConstructor
这样,就可以不通过 vue 组件的声明式调用,而通过命令式调用 NotificationConstructor创建一个新的消息组件实例 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Main from './main.vue'
const NotificationConstructor = Vue.extend(Main) // 利用 Vue.extend 构造一个消息组件构造函数
let instance // 消息组件的实例
let seed = 1 // id 标识种子
const Notification = function (options = {}) {
// ...
// 根据传入的 options 配置示例数据
// 构建一个新的消息组件实例
instance = new NotificationConstructor({
data: options
})
// ...
instance.vm = instance.$mount()
document.body.appendChild(instance.vm.$el)
};

基于此就能构造出统一的新建/删除消息实例方法。这里不再赘述,可以看下面的代码解析部分的详解。

定位计算

接着是定位计算,因为消息组件可能同时存在多个实例,因此需要计算每一实例的具体定位,使之处于正确的位置。
关于这里有两个要求

  • 消息组件有左上左下/右上右下四种定位类型,根据需要灵活配置
  • 具有同样的定位的消息组件实例并不孤立,应该根据先后顺序排列展示

每个消息组件实例之间会相互影响的,所以就需要一个数组存放所有的消息组件实例,以方便进行计算。
当然你也可以对四种定位都分别以一个数组存储处理,不过本身类别不多,单独开4个数组操作起来反而更加麻烦。

虽说要计算定位,实际上四种定位类型,所需要计算的都是 y 轴上的变化再加上区分是从 top 还是 bottom 开始而已,而且 top/bottom 可以根据定位类型推导出来,
所以只要计算 y 轴上的定位偏移值的变化就好了。

因此有两个地方的变化需要计算。

一个是新增实例的时候,需要过滤出所有的相同的定位类型的实例,然后累加已有的实例的实际高度与实例之间的间隔,就可确定新实例的定位偏移值,
相同定位类型的实例就可以按顺序排列展示出来了。

1
2
3
4
5
6
7
// 每个相同 position 的实例相距 16px
let verticalOffset = instances
.filter(item => item.position === position)
.reduce((accOffset, { $el }) => accOffset + $el.offsetHeight + 16, options.offset || 0)
verticalOffset += 16
instance.verticalOffset = verticalOffset

另一个自然是删除实例的时候,这个时候与新增始不同的是,该实例之后的所有相同定位类型的实例的定位偏移值,都需要减去该实例的实际高度与间隔,

1
2
3
4
5
6
7
8
9
10
11
12
// 获取被关闭实例的定位类型 position,与偏移类型 verticalProperty
const { position, verticalProperty } = closeInstance
// 获得被关闭实例的实际高度
const removedHeight = closeInstance.dom.offsetHeight
// 从被关闭的实例索引位置开始,遍历相同定位类型的实例,重新计算其定位偏移值
for (let i = index; i < len - 1; i++) {
if (instances[i].position === position) {
instances[i].dom.style[verticalProperty] =
parseInt(instances[i].dom.style[verticalProperty], 10) - removedHeight - 16 + 'px'
}
}

其他的都是一些小东西,看源码解析就好=。=

源码解析

main.js

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
import Vue from 'vue'
import Main from './main.vue'
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (obj, key) => hasOwnProperty.call(obj, key)
const isVNode = (node) => typeof node === 'object' && hasOwn(node, 'componentOptions')
const NotificationConstructor = Vue.extend(Main) // 利用 Vue.extend 构造一个消息组件构造函数
let instance // 消息组件的实例
const instances = [] // 消息组件实例队列
let seed = 1 // id 标识种子
const Notification = function (options = {}) {
const { onClose: userOnClose, position = 'top-right' } = options
const id = `notification_${seed++}` // 构造标识 id
// 将用户的 close 回调传入到统一的关闭函数中进行处理
options.onClose = () => Notification.close(id, userOnClose)
// 构建一个新的消息组件实例
instance = new NotificationConstructor({
data: options
})
// 判断 message 是否是虚拟 dom 节点,如果是则传入到组件的默认插槽中
if (isVNode(options.message)) {
instance.$slots.default = [options.message]
options.message = ''
}
// 配置实例的各种信息,并初始化插入到 body 中
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 = 99
// 筛选出实例数组中 position 一致的实例,并遍历求和计算出新的消息组件实例的具体偏移值。
// options.offset 可传入一个自定义的偏移值,默认是 0
// 每个相同 position 的实例相距 16px
let verticalOffset = instances
.filter(item => item.position === position)
.reduce((accOffset, { $el }) => accOffset + $el.offsetHeight + 16, options.offset || 0)
verticalOffset += 16
instance.verticalOffset = verticalOffset
// 添加到实例中
instances.push(instance)
// 返回 vue 实例
return instance.vm
};
// 遍历生成多个特定 type 的消息组件的快捷调用函数
['success', 'warning', 'info', 'error']
.forEach(type => Notification[type] = options => {
if (typeof options === 'string' || isVNode(options)) {
options = {
message: options
}
}
options.type = type
return Notification(options)
})
/**
* 消息组件实例的统一关闭函数
* @param {String} id 消息组件的标识 id
* @param {Function|null} userOnClose 用户自定义的关闭回调方法
*/
Notification.close = function (id, userOnClose) {
let index = -1
const len = instances.length
// 过滤出需要关闭的实例,并记录索引值
const closeInstance = instances.filter((item, i) => {
if (item.id === id) {
index = i
return true
}
return false
})[0]
// 过滤实例不存在的情况
if (!closeInstance) return
// 判断用户是否有传入正确 userOnClose ,有则调用该函数
if (typeof userOnClose === 'function') {
userOnClose(closeInstance)
}
// 从实例数组中删除对应的实例
instances.splice(index, 1)
// 过滤实例数组只剩一个或者没有实例的情况
if (len <= 1) return
// 获取被关闭实例的定位类型 position,与偏移类型 verticalProperty
const { position, verticalProperty } = closeInstance
// 获得被关闭实例的实际高度
const removedHeight = closeInstance.dom.offsetHeight
// 从被关闭的实例索引位置开始,遍历相同定位类型的实例,重新计算其定位偏移值
for (let i = index; i < len - 1; i++) {
if (instances[i].position === position) {
instances[i].dom.style[verticalProperty] =
parseInt(instances[i].dom.style[verticalProperty], 10) - removedHeight - 16 + 'px'
}
}
}
export default Notification