封装Vue动态toast组件

2,426 阅读3分钟

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'
    }
  }