通过函数调用创建组件在项目当中应用场景非常广泛,本文将element-plus的ElMessage组件部分功能
了解 h 函数
h函数作用是创建虚拟dom
!注意, 组件和普通节点创建出来的虚拟dom有一部分区别
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)
封装Message组件
文件结构
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, 需要创建的组件不一定是自己封装的