message、notification、toast组件都是很常见的动态组件,但是在Vue中的封装还是有些些的技巧(对我这只菜鸡而言,也不是一般的挑战)
Vue.extend()
如果希望在Vue中实现动态组件,并通过this.$toast()的方式调用组件的话,需要通过Vue.extend()
Vue.extend()是Vue组件的构造器,除了可以像官方例子直接传入组件对象,我们也可以直接传入组件
import Vue from 'vue'
import Toast from './Toast.vue'
const ToastConstructor = Vue.extend(Toast)
// 创建toast实例
const instance = new ToastConstructor()
// 挂载toast实例
instance.$mount()
// 将toast实例添加至body
document.body.appendChild(instance.$el)
👆👆👆以上代码就是我们的toast组件动态构建方式👆👆👆
但是想通过this.$toast()的方式调用,我们还需要这么做
//./Toast/index.js
import Vue from 'vue'
import toast from './Toast.vue'
const Toast = function (option) {
const ToastConstructor = Vue.extend(toast)
// 创建toast实例
const instance = new ToastConstructor({
data: option
// 在element源码中,option作为组件的data值传入
})
// 挂载toast实例
instance.$mount()
// 将toast实例添加至body
document.body.appendChild(instance.$el)
return instance
}
export deault Toast
// 在main.js中引入Toast,并配置
import Toast from './Toast/index.js'
Vue.prototype.$toast = Toast
搭好了toast的架子,现在我们再给toast再添点样式,就初具雏形啦~
不过我们仍需要实现以下功能:
- 定时移除toast
- toast的动画效果
- toast支持回调函数
- 不同种类的toast,
this.$toast.erro()的链式调用 - 页面存在多个toast时的处理
- 不同位置出现toast
参考elementUI实现toast
在element的源码中,message和notification有着相似的骨架,但是在细节上的实现优有所差异
实现toast定时移除
在element源码中,组件的生存时长由duration决定
mounted() {
// 挂载后开启计时器,通过clearTimer取消计时
this.startTimer();
},
methods: {
clearTimer() {
clearTimeout(this.timer);
},
//计时器由duration值控制
startTimer() {
if (this.duration > 0) {
this.timer = setTimeout(() => {
if (!this.closed) {
// this.closed 值由 this.close函数修改
this.close();
}
}, this.duration);
}
},
close() {
this.closed = true;
if (typeof this.onClose === 'function') {
// 执行onClose, 在Message构建时作为option.onClose传入
this.onClose(this);
}
},
handleLeave () {
// tranistion组件的afterLeave回调
// 组件在动画效果结束后销毁
this.$destroy(true)
this.$el.parentNode.removeChild(this.$el)
}
},
watch: {
closed(newVal) {
if (newVal) {
// this.closed的值被修改后,组件的将不可见,触发trasition
this.visible = false;
}
}
},
toast的动画效果实现
可以使用vue的<transition>,由v-show控制组件出现及消失时的动画效果
<transition name='fade'>
<div v-show='visible'></div>
</transition>
.fade-enter,
.leave-active {
opacity: 0;
// 组件在页面的居中展示,是通过translateX(-50%)实现的
transform: translate(-50%, -100%);
}
toast回调函数的绑定及执行
在element源码中,组件的回调函数作为构建输入的option,被绑定在组件的onClose方法上
import Vue from 'vue';
import message from './main.vue';
let MessageConstructor = Vue.extend(message);
let instance;
let instances = [];
const Message = function(options) {
options = options || {};
// 如果调用时只传入了string,则只设置组件data的message部分
if (typeof options === 'string') { options = {message: options}}
let userOnClose = options.onClose
let id = 'message_' + seed++
// 设置组件内的onClose
options.onClose = function() {
// 找到当前实例,并绑定onClose函数
Message.close(id, userOnClose)
}
// 创建实例
instance = new MessageConstructor({data: options})
instance.$mount()
document.body.appendChild(instance.$el)
instances.push(instance)
return instance
};
在构建实例前,通过调用Message.close(),Message组件的onClose函数得到了包装
// 在Message组件关闭时,需要调用对应的回调函数
Message.close = function(id, userOnClose) {
let len = instances.length;
let index = -1;
//首先需要在Message实例数组中找到对应的实例,并实现回调
for (let i = 0; i < len; i++) {
if (id === instances[i].id) {
index = i
if (typeof userOnClose === 'function') {
userOnClose(instances[i])
}
instances.splice(i, 1)
break
}
}
}
不同种类的toast,$toast的链式调用
在调用this.$toast()时,除了将type作为参数传入,还可以使用this.$toast.erro()的调用形式
const typelist = ['success', 'warning', 'info', 'error']
typeList.forEach((item) => {
Message[item] = function (option) {
// 如果只传入字符串,则将option的message属性设置为该字符串
if (typeof option === 'string') { option = { message: option }}
// 设置传入option的type属性
option.type = type
// 再次调用Message函数,将新的option传入
return Message(option)
}
})
页面存在多个toast时的处理
element源码中,通过instances数组实现了对instance的管理
import Vue from 'vue';
import message from './main.vue';
let MessageConstructor = Vue.extend(message);
// 存放当前的实例
let instance;
// 存放全部实例
let instances = [];
// 生成实例的id
let seed = 1;
const Message = function(options) {
// 通过id对实例进行标记
const id = 'message_' + seed++
// 设置组件内的onClose
options.onClose = function() {
// 找到当前实例,并绑定onClose函数
Message.close(id, userOnClose)
}
// 创建实例
instance = new MessageConstructor({data: options})
instance.id = id
instance.$mount()
document.body.appendChild(instance.$el)
// 将实例存入instances
instances.push(instance)
return instance
};
如果限制每次只出现一个toast,设置一个currentToast,并在生成toast前进行判断:如果currentToast存在,那么则不能生成新的toast
不同位置出现toast
再通过instances数组实现了对instance的管理之后,我们需要对多个toast出现时的位置进行规划,并且在toast消失之后,重新规划尚未消失的toast间的位置关系
在element源码中,Message组件挂载并添加到body之后,会根据instances数组中的组件位置,重新对该实例的位置进行定位,并在定位之后,再设置instance.visible = true
// 默认设置的offset是20px
let verticalOffset = option.offset || 20
instances.forEach(item => {
// 新增的元素,offset排在后面
verticalOffset += item.$el.offsetHeight + 16
})
// 此时设置Offset
instance.verticalOffset = verticalOffset
// 在组件内的visiable是false,此时改为true,组件渲染
instance.visible = true
// 设置zIndex,保证实例的zIndex在最高级
instance.$el.style.zIndex = ....
在instance销毁之后,也会重新对instances中的组件位置进行重排
// 在onClose函数中进行定义
Message.close = function(id, userOnClose) {
const len = instances.length;
let removedHeight
let index = -1;
// 首先需要在Message实例数组中找到对应的实例,并确定该元素的offset值
for (let i = 0; i < len; i++) {
if (id === instances[i].id) {
index = i
removedHeight = instances[i].$el.offsetHeight
instances.splice(i, 1)
break
}
}
// 重新对删除的instance之后的组件进行位置排序
if (len <= 1 || index === -1 || index > instances.length - 1) return
// 修改后续的组件呈现位置
for (let i = index; i < len - 1; i++) {
const dom = instances[i].$el
dom.style.top =
parseInt(dom.style.top, 10) - removedHeight - 16 + 'px'
}
}
elementUI中,Notification组件和Message组件类似,但是设置了不同的出现位置,这一功能通过设置组件的posiotion实现。在进行定位时,需要在instances内筛选出具有相同posiotion属性的组件,并根据组件的位置进行重新排序
// 呈现时
const position = instance.position
let verticalOffset = option.offset || 0
// 设置组件的呈现高度,根据不同的位置
instances.filter((item) => {
item.position === position
}).forEach(
(item) => {
verticalOffset += item.$el.offsetHeight + 16
}
)
// 实际不是吸顶呈现的,距离顶部是16px
verticalOffset += 16
instance.verticalOffset = verticalOffset
instance.visible = true
instance.$el.style.zIndex = 100
// 销毁后
if (length <=1 || index === -1 || index > instances.length - 1) return
// 修改index之后的样式
const position = instance.position
const removedHeight = instance.dom.offsetHeight
const verticalType = instance.verticalProperty
for(let i = index; i < length-1; i++) {
if (instance[i].position === position) {
const dom = instances[i].$el
dom.style[verticalType] = parseInt(dom.style[verticalType], 10) - removedHeight - 16 + 'px'
}
}