Vue进阶(二)设计高级组件——自定义通知

news/2024/7/10 2:08:53 标签: vue

阅读笔者的这一篇文章需要读者具有一定的Vue组件知识基础,如果本篇文章存在设计不合理或者知识错误的情况,还恳请指出修正。

1. 期望:

在这里插入图片描述
当我们需要向客户端提示一些信息时,我们希望这个提示能在页面的右下角显出,如果同时存在多个通知的时候,这些通知会自动向上叠,并且每个通知可以在一定时间后自动消失。
为了让这个通知组件可以在任意界面都能向用户提示一些内容,并且同时可以存在多个通知,那么,这个组件就不应该以标签的形式固定在某一个页面中(当然,如果你实在想这样做,也不是不能做到)。如果有用过jquery的经验,我们可能想到借助一个函数来动态生成这个组件,并将组件挂载到页面上。

按照以函数来生成一个组件的思想,设计一个组件我们有一下步骤:

  1. 构建一个组件骨架,里面有该组件的不变属性,如字体大小。
  2. 使用函数,根据使用情况来变动组件的属性,如组件的位置。

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>

结合代码和注释,这里再解释为何要设置verticalOffsetcontentHeight这两个变量。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

http://www.niftyadmin.cn/n/1472956.html

相关文章

语言学习网站-实验楼 第一天Linux复习

发现了一个学习语言不错的地方&#xff0c;可以边看边输入代码测试。还会有一些作业题。和一些比赛&#xff0c;比赛前两名和学习总时间达到3000分钟都可以加入一个百楼俱乐部的机会。第一次比赛 考的是 可变参数&#xff0c;要注意的地方有 可变参数可以传一个数组&#xff0c…

Shell脚本一次执行多条命令

Shell 是一个用 C 语言编写的程序&#xff0c;它是用户使用 Linux 的桥梁。Shell 既是一种命令语言&#xff0c;又是一种程序设计语言。 Shell 是指一种应用程序&#xff0c;这个应用程序提供了一个界面&#xff0c;用户通过这个界面访问操作系统内核的服务。 简要来讲&#xf…

Slam基础 三维空间运动

三维空间刚体运动 旋转矩阵 点和向量,坐标系 向量 a a a在线性空间的基 [ e 1 , e 2 , e 3 ] [e_1, e_2, e_3] [e1​,e2​,e3​]下的坐标为 [ a 1 , a 2 , a 3 ] T [a_1, a_2, a_3]^T [a1​,a2​,a3​]T. a [ e 1 , e 2 , e 3 ] [ a 1 a 2 a 3 ] a 1 e 1 a 2 e 2 a 3 e 3 …

『OpenCV3』简单图片处理

cv2和numpy深度契合&#xff0c;其图片读入后就是numpy.array&#xff0c;只不过dtype比较不常用而已&#xff0c;支持全部数组方法 数组既图片 import numpy as np import cv2 img np.zeros((3, 3), dtypenp.uint8) # numpy数组使用np.uint8编码就是cv2图片格式 print(img, …

【原理】CAM(类激活映射)模型可视化

前言 深度学习如火如荼&#xff0c;神经网络的可解释性一直是讨论的热点&#xff0c;其实可解释性常和模型的可视化联系在一起,模型可视化或者模型可解释说到是对某一类别具有可解释性&#xff0c;直接画出来特征图并不能说明模型学到了某种特征。这里主要讲一下基于类激活映射…

[Leetcode] Multiply strings 字符串对应数字相乘

Given two numbers represented as strings, return multiplication of the numbers as a string. Note: The numbers can be arbitrarily large and are non-negative. 题意&#xff1a;计算字符串对应数字的乘积。参考JustDoIT的博客。 思路&#xff1a;先用一维向量存放计算…

Vue进阶(三)插槽slot,并使用slot开发高级分页组件

如果对组件不太了解&#xff0c;可以先阅读笔者的这两篇文章&#xff0c;在对组件有了一定的了解之后&#xff0c;在查看本篇文章&#xff1a; vue进阶&#xff08;一&#xff09;&#xff0c;深入了解组件&#xff0c;自定义组件 Vue进阶&#xff08;二&#xff09;设计高级组…

【面试】数据库原理

什么是数据库 数据库简单来讲就是存放数据的地方&#xff0c;面试过程中&#xff0c;数据库一般都会被问到&#xff0c;这里主要是自己查阅的资料和一些总结&#xff0c;主要为了巩固下自己的对数据库的理解和认识。这里主要结合MySQL数据库进行整理。 存储过程&#xff08;Sto…