阅读笔者的这一篇文章需要读者具有一定的Vue组件知识基础,如果本篇文章存在设计不合理或者知识错误的情况,还恳请指出修正。
1. 期望:
当我们需要向客户端提示一些信息时,我们希望这个提示能在页面的右下角显出,如果同时存在多个通知的时候,这些通知会自动向上叠,并且每个通知可以在一定时间后自动消失。
为了让这个通知组件可以在任意界面都能向用户提示一些内容,并且同时可以存在多个通知,那么,这个组件就不应该以标签的形式固定在某一个页面中(当然,如果你实在想这样做,也不是不能做到)。如果有用过jquery的经验,我们可能想到借助一个函数来动态生成这个组件,并将组件挂载到页面上。
按照以函数来生成一个组件的思想,设计一个组件我们有一下步骤:
- 构建一个组件骨架,里面有该组件的不变属性,如字体大小。
- 使用函数,根据使用情况来变动组件的属性,如组件的位置。
2. 构建Vue组件
notificationInit.vue:
<!--创建组件的骨架结构,包括组件中数据的声明-->
<template>
<transition name="fade" @after-leave="afterLeave">
<div
class="notification"
:style="style"
v-show="visible"
@mouseenter="clearTimer"
@mouseleave="createTimer"
>
<span class="content">{{content}}</span>
<a class="btn" @click="handleClick">{{btn}}</a>
</div>
</transition>
</template>
<script>
export default {
name: "Notification",
props: {
content: {
//通知的具体信息
type: String,
required: true
},
btn: {
//用户的操作按钮提示文本,默认为“关闭”
type: String,
default: '关闭'
},
autoClose:{
//是否启用自动关闭
type: Boolean,
default: true
},
autoCloseTime: {
type: Number,
default: 3000,
validate(value){
if (value < 0) return false
}
},
response:{
type: Function,
default: null
}
},
data () {
return {
visible: false,
//verticalOffset,组件到底部的距离
verticalOffset: 0,
//当组件要被关闭时,记录组件之前占用的高度
contentHeight: 0
}
},
computed: {
style() {
return {
position: 'fixed',
right: '20px',
bottom: `${this.verticalOffset}px`
}
}
},
methods: {
createTimer() {
//如果设置了自动关闭,则使用setTimeout来关闭
//这个方法会在组件创建时和鼠标离开通知组件时调用
if (this.autoClose){
this.timer = setTimeout(() => {
this.contentHeight = this.$el.offsetHeight
this.visible = false
}, this.autoCloseTime)
}
},
clearTimer(){
//当鼠标处于通知上方时,如果设置了自动关闭,则清除计时器
if (this.timer){
clearTimeout(this.timer)
}
},
handleClick(e){
//处理点击事件,并且隐藏通知组件,设置contentHeight
e.preventDefault()
if (this.response){
this.response()
}
this.contentHeight = this.$el.offsetHeight
this.visible = false
},
afterLeave () {
//当transition的动画结束之后,触发closed事件
this.$emit('closed', this.contentHeight)
}
},
mounted() {
if (this.autoClose) this.createTimer()
},
beforeDestroy() {
this.clearTimer()
}
}
</script>
<style lang="scss" scoped>
.notification{
display: inline-flex;
background-color: #303030;
color: rgba(255,255,255,1);
align-items: center;
padding: 20px;
min-width: 280px;
box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12);
flex-wrap: wrap;
transition: all .3s;
.content{
padding: 0;
}
.btn{
color: #ff4081;
padding-left: 24px;
margin-left: auto;
cursor: pointer;
}
}
</style>
结合代码和注释,这里再解释为何要设置verticalOffset和contentHeight这两个变量。verticalOffset决定了通知栏到页面底部的距离,因为如果我们存在多个通知组件的时候,我们需要将新的组件放在最上方;而当我们点击关闭一个组件时,其他组件的位置也会相应的发生变化(当最下面的组件关闭时,在它上面的通知组件需要“掉下去”),这时候我们需要知道被关闭的组件的内容高度offertHeight,们这里需要使用contentHeight来保存offsetHeight而不是直接使用offsetHeight的原因在后文中有详细说明,这里我们只需要知道,contentHeight就是为了保存组件未关闭时的offsetHeight值。
3. 通过函数创建组件
notification.js:
import NotificationInit from "./notificationInit";
const NotificationConstructor = Vue.extend(NotificationInit)
const notify = (options) => {
//如果是服务器渲染,则不执行
if (Vue.prototype.$isServer) return
const instance = new NotificationConstructor()
instance.$mount()
document.body.appendChild(instance.$el)
instance.visible = true
return instance
}
export default notify
这段代码很容易理解,NotificationConstructor 继承自NotificationInit,然后我们通过new NotificationConstructor()
来创建一个组件,并将创建出来的组件instance挂载到body上面。
在创建这个组件时,我们需要通过props来传递参数content,btn等,同时,我们需要根据此时存在的NotificationConstructor实例来修改instance的位置(即其verticalHeight属性),为了区分每一个通知组件,我们为其动态生成一个id :
const instances = []
let seed = 0
const notify = (options) => {
if (Vue.prototype.$isServer) return
const instance = new NotificationConstructor({
propsData: options
})
instance.id = `notification_${seed++}`
//这是vertical.offsetHeight,每个通知组件都间隔其下方的组件16px
let verticalOffset = 16
instances.forEach( item => {
verticalOffset += item.$el.offsetHeight + 16
})
instance.verticalOffset = verticalOffset
instance.$mount()
document.body.appendChild(instance.$el)
instance.visible = true
instances.push(instance)
return instance
}
为Vue挂载一个全局函数:
import notify from "./components/notification/notification";
Vue.prototype.$notify = notify
现在我们可以在应用的任意位置调用notify来增加一个通知了。
this.$notify({
content: '这是一个通知',
autoClose: false,
response: function (){
console.log('这个通知需要被用户点击才会关闭')
}
})
现在我们已经能动态生成通知组件并将其挂载到body上面了。
接下来我们需要完成的是,当我们点击关闭或者通知自动关闭时,其上方的组件能够自动向下补齐。
还记得我们在通知组件中设置了:
afterLeave () {
//当transition的动画结束之后,触发closed事件
this.$emit('closed', this.contentHeight)
}
所以,我们需要在notify中监听 ‘closed’,当通知组件关闭时的transition的动画结束之后,我们需要调整其他通知组件的位置:
import Vue from "vue";
import NotificationInit from "./notificationInit";
const NotificationConstructor = Vue.extend(NotificationInit)
const instances = []
let seed = 0
const removeInstance = (instance, val) => {
if (!instance) return
const index = instances.findIndex( item => item.id === instance.id)
instances.splice(index, 1)
const len = instances.length
if (len === 0) return
for (let i = index; i < len; i ++){
instances[i].verticalOffset = parseInt(instances[i].verticalOffset) - val - 16
}
}
const notify = (options) => {
if (Vue.prototype.$isServer) return
const instance = new NotificationConstructor({
propsData: options
})
instance.id = `notification_${seed++}`
let verticalOffset = 16
instances.forEach( item => {
verticalOffset += item.$el.offsetHeight + 16
})
instance.verticalOffset = verticalOffset
instance.$on('closed', (val) => {
removeInstance(instance, val)
document.body.removeChild(instance.$el)
instance.$destroy()
})
instance.$mount()
document.body.appendChild(instance.$el)
instance.visible = true
instances.push(instance)
return instance
}
export default notify
观察这一段代码:
instance.$on('closed', (val) => {
removeInstance(instance, val)
document.body.removeChild(instance.$el)
instance.$destroy()
})
removeInstance是修改其他组件的verticalOffset值,以此来改变组件的位置,使组件补齐。但我们还需要document.body.removeChild(instance.$el); instance.$destroy()
,removeChild可以将组件从DOM中移出,destroy则用于销毁此vue实例,这两步是为了内存优化并降低性能损耗。
我们现在再来查看removeInstance:
const removeInstance = (instance, val) => {
if (!instance) return
const index = instances.findIndex( item => item.id === instance.id)
instances.splice(index, 1)
const len = instances.length
if (len === 0) return
for (let i = index; i < len; i ++){
instances[i].verticalOffset = parseInt(instances[i].verticalOffset) - val - 16
}
}
这段代码很容易理解,需要我们注意的是,为什么我们这里传入的val是我们自己定义的contentHeight而不是直接使用offsetHeight?
我们调用removeInstance,是因为组件 $emit(‘closed’),也就是此时组件的transition动画已经结束,这时候组件的offsetHeight已经变为0,不再是之前通组组件存在时的内容高度了。
4. 源码:
notificationInit.vue
<!--创建组件的骨架结构,包括组件中数据的声明-->
<template>
<transition name="fade" @after-leave="afterLeave">
<div
class="notification"
:style="style"
v-show="visible"
@mouseenter="clearTimer"
@mouseleave="createTimer"
>
<span class="content">{{content}}</span>
<a class="btn" @click="handleClick">{{btn}}</a>
</div>
</transition>
</template>
<script>
export default {
name: "Notification",
props: {
content: {
//通知的具体信息
type: String,
required: true
},
btn: {
//用户的操作按钮提示文本,默认为“关闭”
type: String,
default: '关闭'
},
autoClose:{
//是否启用自动关闭
type: Boolean,
default: true
},
autoCloseTime: {
type: Number,
default: 3000,
validate(value){
if (value < 0) return false
}
},
response:{
type: Function,
default: null
}
},
data () {
return {
visible: false,
//verticalOffset,组件到底部的距离
verticalOffset: 0,
//当组件要被关闭时,记录组件之前占用的高度
contentHeight: 0
}
},
computed: {
style() {
return {
position: 'fixed',
right: '20px',
bottom: `${this.verticalOffset}px`
}
}
},
methods: {
createTimer() {
//如果设置了自动关闭,则使用setTimeout来关闭
//这个方法会在组件创建时和鼠标离开通知组件时调用
if (this.autoClose){
this.timer = setTimeout(() => {
this.contentHeight = this.$el.offsetHeight
this.visible = false
}, this.autoCloseTime)
}
},
clearTimer(){
//当鼠标处于通知上方时,如果设置了自动关闭,则清除计时器
if (this.timer){
clearTimeout(this.timer)
}
},
handleClick(e){
//处理点击事件,并且隐藏通知组件,设置contentHeight
e.preventDefault()
if (this.response){
this.response()
}
this.contentHeight = this.$el.offsetHeight
this.visible = false
},
afterLeave () {
//当transition的动画结束之后,触发closed事件
this.$emit('closed', this.contentHeight)
}
},
mounted() {
if (this.autoClose) this.createTimer()
},
beforeDestroy() {
this.clearTimer()
}
}
</script>
<style lang="scss" scoped>
.notification{
display: inline-flex;
background-color: #303030;
color: rgba(255,255,255,1);
align-items: center;
padding: 20px;
min-width: 280px;
box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12);
flex-wrap: wrap;
transition: all .3s;
.content{
padding: 0;
}
.btn{
color: #ff4081;
padding-left: 24px;
margin-left: auto;
cursor: pointer;
}
}
</style>
notification.js
import Vue from "vue";
import NotificationInit from "./notificationInit";
const NotificationConstructor = Vue.extend(NotificationInit)
const instances = []
let seed = 0
const removeInstance = (instance, val) => {
if (!instance) return
const index = instances.findIndex( item => item.id === instance.id)
instances.splice(index, 1)
const len = instances.length
if (len === 0) return
for (let i = index; i < len; i ++){
// console.log(removeHeight)
instances[i].verticalOffset = parseInt(instances[i].verticalOffset) - val - 16
}
}
const notify = (options) => {
if (Vue.prototype.$isServer) return
const instance = new NotificationConstructor({
propsData: options
})
instance.id = `notification_${seed++}`
instance.$mount()
document.body.appendChild(instance.$el)
instance.visible = true
let verticalOffset = 16
instances.forEach( item => {
verticalOffset += item.$el.offsetHeight + 16
})
instance.verticalOffset = verticalOffset
instance.$on('closed', (val) => {
removeInstance(instance, val)
document.body.removeChild(instance.$el)
instance.$destroy()
})
instances.push(instance)
return instance
}
export default notify
为vue全局挂载notify():
import notify from "./components/notification/notification";
Vue.prototype.$notify = notify