vue全局通知组件

3,512 阅读4分钟

这是一个通知组件,在其他组件内可以通过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
  }
}

嗯?删除组件后上面的组件没有下来,就是位置没有变化

原来divv-show时根据visible判断的,在默认组件里永远是true,在func-notification.js里没有改这个值,所以还是true,所以组件一声明的时候它的v-show条件就是成立的,这个时候是不会触发transition的,导致的结果就是after-enter根本不会被触发。

这个时候可以在func-notification.js中声明visiblefalse,在function.js中把dom节点插入到文档中之💰,设置实例的visibletrue

// 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属性即使会被扩展组件覆盖也要在组件内声明,防止单独复用组件时报错

以上就是全局通知组件的创建过程了