这是一个通知组件,在其他组件内可以通过this.$notify()调用,如果只是封装一个组件,在其他组件中通过引用、注册的方式使用其实很简单,这里想要通过调用方法的方式引用组件
组件基本构成
一个基础的通知组件由2部分组成,一个是通知内容,另一个是关闭按钮。子组件的数据应该来源于父组件,子组件通过props接收,给props传递的参数添加一些验证,也方便如果数据类型不符合时抛出异常。利用transition动画使组件的显示、消失不那么突兀
<template>
<transition name="fade">
<div class="notification">
<span>{{content}}</span>
<a class="btn" @click="handleClose">{{btn}}</a>
</div>
</transition>
</template>
<script>
export default {
name: 'notification',
props: {
content: {
type: String,
required: true
},
btn: {
type: String,
default: '关闭'
}
}
}
</script>
现在完成了最基本的组件形式,但是要把通知组件作为一个全局性通用的组件,而且可以发布到第三方的组件,这边会提供一个类似vue插件的使用方法
注册组件
新建一个index.js组件,导出一个方法,这个方法在使用Vue.install()会接收一个Vue参数,然后把通知组件在全局注册一下,这样在每个组件都可以调用。在定义组件的时候最好为每个组件定义一个name,因为当有很多组件的时候,如果每个组件都在注册的时候用字符串去定义,那样不好维护
// index.js
import Notification from './notification.vue'
export default (Vue) => {
Vue.component(Notification.name, Notification)
}
然后在main.js中通过Vue.use的方式注册插件
//main.js
import Notification from './components/notification'
Vue.use(Notification)
这样这个组件就定义在全局了,可以在然后地方使用这个组件
<notification content="notification"></notification>
但像现在这样并不是我想要的效果,因为这样使用时还需要在组件内判断什么时候显示,什么时候消失,我想用调用API的方式调用,那接着往下看
扩展组件,基础组件中的属性是不够用的,那为什么不写在组件内呢?因为那样修改后通过引用注册的方式使用组件时组件会变得不好用,所以通过扩展来达到我们的目的
新建一个func-notification.js
import Notification from './notification.vue'
export default {
extends: Notification,
}
新建一个function.js文件写可以调用notify的方法,因为这是个方法,我们要通过js的方法去创建一个Vue组件,Vue.extend可以创建一个组件构造器,然后可以通过new这个构造器创建一个组件
import Vue from 'vue'
import Component from './func-notification'
const NotificationConstructor = Vue.extend(Component)
const notify = (options) => {
// 因为这里涉及到dom操作,如果是在服务端是没办法操作dom的
if (Vue.prototype.$isServer) return
const instance = new NotificationConstructor({})
}
如果要插入dom到body中,还要考虑组件的样式定位,所以在组件中加一个样式
<div class="notification" :style="style">
</div>
为了组件不报错,要在组件内声明style,返回一个空对象
computed: {
style () {
return {}
}
},
然后可以在func-notification.js中声明这个style覆盖组件内的style,位置要根据当前的通知组件数量来判断,所以在function.js中添加一个instances记录组件实例,然后为每个实例添加一个id,以便后来删除组件的时候能通过id找到,然后调用实例的$mount(),调用$mount()没传入节点时只是生成了一个$el对象,没有真正插入到dom中去,最后通过appendChild插入到body中
const instances = []
let _seed = 1
const notify = (options) => {
//...
const instance = new NotificationConstructor({
propsData: options
})
const id = `notification${_seed++}`
instance.id = id
instance.vm = instance.$mount()
document.body.appendChild(instance.vm.$el)
}
我们默认把所有的通知放在最下面,如果出现新的通知,就把第一个顶上去,最后导出notify这个方法
// function.js
const notify = (options) => {
// ...
let verticalOffset = 0
instances.forEach(item => {
verticalOffset += item.$el.offsetHeight + 16
})
// 默认比屏幕边框高16px
verticalOffset += 16
instance.verticalOffset = verticalOffset
instances.push(instance)
return instance.vm
}
export default notify
根据verticalOffset计算组件的位置
// func-notification.js
computed: {
style () {
return {
position: 'fixed',
right: '20px',
bottom: `${this.verticalOffset}px`
}
}
}
data () {
return {
verticalOffset: 0
}
}
在index.js中,给Vue原型加上$notify这个方法,
import Notification from './notification.vue'
import notify from './function'
export default (Vue) => {
Vue.component(Notification.name, Notification)
Vue.prototype.$notify = notify
}
定时关闭组件
一般的通知组件显示几秒之后就会自动关闭,所以在mounted生命周期中创建定时器,组件隐藏也要通过transition的方式去做,那就需要某个属性去控制这个组件的显示与否,所以在组件中加上visible属性控制组件的显示和隐藏
<div class="notification" :style="style" v-show="visible">
</div>
在组件销毁时清除定时器
// func-notification.js
mounted () {
this.createTimer()
},
beforeDestory () {
this.clearTimer()
},
methods: {
createTimer () {
if (this.autoClose) {
this.timer = setTimeout(() => {
this.visible = false
}, this.autoClose)
}
},
clearTimer () {
clearTimeout(this.timer)
},
}
data () {
return {
verticalOffset: 0,
autoClose: 3000,
}
}
如果想要autoClose从外面传进来,可以通过解构options的方式
// function.js
const { autoClose, ...rest } = options
const instance = new NotificationConstructor({
propsData: {
...rest
},
data: {
autoClose: autoClose === undefined ? 3000 : autoClose
}
})
这样让节点消失的方式只是把节点的样式设置为display:none,并没有让节点真正的消失,所以要让节点在文档里真正的消失,同时把vm对象删除
删除节点的时机不能时将visible设置为false的时候,因为那个时候动画还没有结束,所以要在动画结束的时候删除节点,为transition添加一个动画结束的事件
<transition name="fade" @after-leave="afterLeave"></transition>
动画结束时触发closed方法,
// notification.vue
methods: {
handleClose (e) {
e.preventDefault()
// 将要关闭这个组件
this.$emit('close')
},
afterLeave () {
// 已经关闭这个组件
this.$emit('closed')
},
}
然后可以在外部监听这个事件
// function.js
const removeInstance = (instance) => {
if (!instance) return
const index = instances.findIndex(inst => instance.id === inst.id)
instances.splice(index, 1)
}
const notify = (options) => {
// ...
instance.vm.$on('closed', () => {
removeInstance(instance)
document.body.removeChild(instance.vm.$el)
// 实例可以彻底销毁
instance.vm.$destroy()
})
}
手动删除组件
设置点击close按钮可以删除通知组件
// function.js
instance.vm.$on('close', () => {
instance.vm.visible = false
})
关闭通知时,上面的通知节点可以下移
在removeInstance时,调整其他组件的高度,首先在继承的组件的data中声明一个height,在节点渲染成功的时候,给height设置值,因为即便组件可以显示了,在经历transition动画时是拿不到height的,所以要等到动画结束后才能拿到height,所以给动画添加一个事件after-enter,在组件声明一下afterEnter方法,否则复用组件时会报错
<transition name="fade" @after-leave="afterLeave" @after-enter="afterEnter"></transition>
在继承的组件中定义这个方法
// func-notification.js
afterEnter () {
// 获取实际高度,因为计算其他高度时要减去删除节点的实际高度
this.height = this.$el.offsetHeight
}
// function.js
const removeInstance = (instance) => {
if (!instance) return
const len = instances.length
const index = instances.findIndex(inst => instance.id === inst.id)
instances.splice(index, 1)
// 如果只有一个节点,那被删除就可以直接返回
if (len <= 1) return
const removeHeight = instance.vm.height
// 从被删除的节点开始计数
for (let i = index; i < len - 1; i++) {
instances[i].verticalOffset = parseInt(instances[i].verticalOffset) - removeHeight - 16
}
}
嗯?删除组件后上面的组件没有下来,就是位置没有变化
原来div的v-show时根据visible判断的,在默认组件里永远是true,在func-notification.js里没有改这个值,所以还是true,所以组件一声明的时候它的v-show条件就是成立的,这个时候是不会触发transition的,导致的结果就是after-enter根本不会被触发。
这个时候可以在func-notification.js中声明visible为false,在function.js中把dom节点插入到文档中之💰,设置实例的visible为true
// func-notification.js
data () {
return {
verticalOffset: 0,
autoClose: 3000,
height: 0,
visible: false
}
}
//function.js
instance.vm.visible = true
鼠标移上去后组件不会自动消失,鼠标移开后组件重新计时,然后自动消失
在组件div上加鼠标移入移出事件
<div class="notification" :style="style" v-show="visible" @mouseenter="clearTimer" @mouseleave="createTimer">
<span>{{content}}</span>
<a class="btn" @click="handleClose">{{btn}}</a>
</div>
要注意:有些方法或者data属性即使会被扩展组件覆盖也要在组件内声明,防止单独复用组件时报错
以上就是全局通知组件的创建过程了