vue3.0 函数创建组件,实现ElMessage

128 阅读1分钟

通过函数调用创建组件在项目当中应用场景非常广泛,本文将element-plus的ElMessage组件部分功能

20240318_165216.gif

了解 h 函数

h函数作用是创建虚拟dom

Snipaste_2024-03-18_17-09-31.png

!注意, 组件和普通节点创建出来的虚拟dom有一部分区别

Snipaste_2024-03-18_17-07-41.png

h函数本质上是createVnode的二次封装,处理了参数

了解render函数

// 创建div节点
let container = document.createElement('div')
// 创建虚拟dom
let vnode = h('h1')
// vnode编译成真实dom节点,并插入到container中
render(vnode, container)
console.log(vnode)
-----------------------------------------------------------
//从container清除dom节点
render(null, container)

Snipaste_2024-03-18_17-42-11.png

封装Message组件

文件结构 微信截图_20240318180320.png

Message.vue

<template>
  <transition name="fade" @after-leave="$emit('destroy')" @before-leave="onClose">
    <!-- after-leave 动画结束后
    before-leave 动画结束前 -->
    <div
      ref="messageRef"
      class="message"
      @mouseenter="stop"
      @mouseleave="start"
      v-show="visible"
      :style="style"
    >
      <p>{{ message }}</p>
    </div>
  </transition>
</template>

<script setup>
//https://www.vueusejs.com/functions.html    vueuse,非常好用的vue3.0库
import { useElementSize, useTimeoutFn } from '@vueuse/core'
import { getLastOffset } from './method.js'

let props = defineProps({
  message: String, //显示内容
  id: String, //唯一标识
  duration: {
    //message几秒后消失
    type: Number,
    default: 3000
  },
  onClose: Function //动画结束前回调
})

defineEmits(['destroy'])

//控制显示隐藏
let visible = ref(false)
let messageRef = ref()

// messageRef 高度
let { height } = useElementSize(messageRef)

// 获取上一个message实例的bottom属性
const lastOffset = computed(() => getLastOffset(props.id))

let offset = computed(() => lastOffset.value + 16 * 2)

// 计算当前message高度和向上便宜位置
const bottom = computed(() => height.value + offset.value)

const style = computed(() => ({
  top: offset.value + 'px'
}))

// stop会关闭setTimeout, start重新开始
let { start, stop } = useTimeoutFn(() => {
  close()
}, props.duration)

const close = () => {
  visible.value = false
}
onMounted(() => {
  // 等待节点渲染完毕显示才会有动画效果
  visible.value = true
})

// 导出属性,可通过 vnode.component.exposed访问到
defineExpose({
  bottom,
  close,
  visible
})
</script>
<style lang="scss" scoped>
.message {
  position: fixed;
  left: 50%;
  top: 20px;
  z-index: 9999;
  transform: translateX(-50%);
  transition:
    opacity 0.4s,
    transform 0.4s,
    top 0.4s;
  padding: 10px 20px;
  border: 1px solid red;
  background: #67c23a;
  border-radius: 4px;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
  transform: translate(-50%, -100%);
}
</style>

method.js

import { h, render } from 'vue'
import Message from './Message.vue'

//收集每一个message实例
let instances = shallowReactive([])

// 获取当前实例和上个message实例
export const getInstance = (id) => {
  let idx = instances.findIndex((instance) => instance.id === id)
  let prev = undefined
  let current = instances[idx]

  if (idx > 0) {
    prev = instances[idx - 1]
  }

  return { current, prev }
}

//获取上一个message bottom属性
export const getLastOffset = (id) => {
  const { prev } = getInstance(id)
  if (!prev) return 0
  return prev.vnode.component.exposed.bottom.value
}

// 关闭message
export const closeMessage = (id) => {
  let idx = instances.findIndex((instance) => instance.id === id)
  if (idx == -1) return

  // 关闭后从instances移除message
  instances.splice(idx, 1)
}

//生成唯一标识用
let seed = 0

export const message = (options) => {
  let container = document.createElement('div')

  //唯一标识
  let id = `message_${seed++}`

  //通过Message组件创建vnode
  let vnode = h(Message, {
    message: options.message,
    id,
    //动画结束前回调
    onClose() {
      closeMessage(id)
    },
    //动画结束后清除dom节点, 不调用
    onDestroy() {
      render(null, container)
    }
  })

  // 被instances收集
  let instance = {
    id,
    vnode
  }
  instances.push(instance)

  //生成真实dom节点和组件实例(vnode.el和vnode.component)
  render(vnode, container)

  //插入到body当中
  document.body.appendChild(container.firstElementChild)

  const close = () => {
    vnode.component.exposed.close()
  }
  instance.close = close

  // 返回
  return instance
}

index.vue

<template>
  <n-button style="margin-left: 200px" @click="onOpen">Show message</n-button>
</template>

<script setup>
import { message } from './method'

const onOpen = () => {
  message({
    message: 'This is a message'
  })
}
</script>
<style lang="scss" scoped></style>

我们也可以把已经存在的第三方组件利用函数创建, 例如 ElDialog, 需要创建的组件不一定是自己封装的