element 的 notification 消息组件解析

题图无关
这是承继上一篇——不知道哪一篇提到我在公司内部写一套 ui 控件的下一篇。因为最近也需要实现一个类似 element 的 notification 消息组件。
因此便直接研究参考了 element 的实现源码,相对简单,于是写一下记下来。
基本实现原理
总体来说这个组件还是比较简单的。我对比了一下 iview 跟 element 的实现,个人感觉 element
的更好。
组件结构
先来看看这个组件的目录结构。
notification
├── index.js
├── src
| ├── main.js
| └── main.vue
除开统一的暴露文件 index.js, 主要的就是 main.js
和 main.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)
let instance let seed = 1
const Notification = function (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
| 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
| 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)
let instance const instances = [] let seed = 1
const Notification = function (options = {}) { const { onClose: userOnClose, position = 'top-right' } = options const id = `notification_${seed++}`
options.onClose = () => Notification.close(id, userOnClose)
instance = new NotificationConstructor({ data: options }) if (isVNode(options.message)) { instance.$slots.default = [options.message] options.message = '' }
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
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)
return instance.vm };
['success', 'warning', 'info', 'error'] .forEach(type => Notification[type] = options => { if (typeof options === 'string' || isVNode(options)) { options = { message: options } } options.type = type return Notification(options) })
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
if (typeof userOnClose === 'function') { userOnClose(closeInstance) }
instances.splice(index, 1)
if (len <= 1) return
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
|