前言
Message组件,顾名思义,是负责展示和处理信息的模块。它可以被集成到各种应用程序中,为用户提供友好的信息展示界面,以及丰富的交互功能。通过精心设计和开发,Message组件不仅可以提升用户的信息获取效率,还能增强用户的使用体验,从而推动应用程序的整体性能提升。本文将简单介绍Message组件的基本开发。
需求分析
- 在特定时机,弹出一个对应的提示信息
- 提示信息在一定时间之后可以消失
- 提供手动关闭的功能
- 可以弹出多条信息
- 类型(primary,warning,danger等)
- 使用调用函数的方法来创建组件
通过以上分析,发现小小的一个Message组件竟然蕴藏着这么多的功能,尤其还有弹出多条信息和使用函数创建的难点,接下来我们逐一分析。
确定方案
- 属性
import type { VNode, ComponentInternalInstance } from 'vue'
export interface MessageProps {
// 信息文本,可以是简单字符串,也可以是复杂节点
message?: string | VNode
type?: string
icon?: string
// 信息显示持续时间
duration?: number
// 手动关闭
showClose?: boolean
// 偏移
offset?: number
// 添加z-index
zIndex: number
// 手动关闭的方法
useDestroy: () => void
id: string
// 过渡动画的名称
transitionName?: string
}
// 用户创建Message时,被过滤掉的属性是必选的,但是不是由用户添加,而是由内部处理
// 所以将必选的过滤掉
export type CreateMessageProps = Omit<
MessageProps,
'useDestroy' | 'id' | 'zIndex'
>
- 实例属性
export interface MesssageContext {
// 每个Message实例需要一个唯一id
id: string
// 实例对应的已经生成的Message组件节点
vnode: VNode
// 也要具有Message的属性
props: MessageProps
// 可以用过vm拿到Vue组件实例内部属性和方法
vm: ComponentInternalInstance
// 销毁实例
destroy: () => void
}
- 组件
// Message.vue
<template>
<Transition
:name="props.transitionName">
<div class="yv-message">
<!-- 内容区域 -->
<div class="yv-message__content">
<slot>
<RenderVnode />
</slot>
</div>
<!-- 关闭图标 -->
<div class="yv-message__close">
<Icon icon="xmark"></Icon>
</div>
</div>
</Transition>
</template>
代码实现
- 函数式方式创建组件
import { render, h, shallowReactive } from 'vue'
import Message from './Message.vue'
// 用于拿到当前组件的z-index属性的hook函数,后文将展示
import useZIndex from '@/hooks/useMessageZIndex'
import type { CreateMessageProps, MesssageContext } from './types'
let seed = 1
// 主要函数,用户通过此函数进行创建Message
export function YvMessage(props: CreateMessageProps) {
// 即将被创建的组件的z-index
const { nextZIndex } = useZIndex()
// 每个组件有唯一的id
const id = `message_${seed++}`
// 用来防止Message组件的容器
const container = document.createElement('div')
// 在添加组件应有的属性
const newProps = {
...props,
id,
zIndex: nextZIndex(),
// destroyMesssage见下文
useDestroy: destroyMesssage
}
const vnode = h(Message, newProps)
// 通过render函数将虚拟DOM节点渲染或挂载到真实DOM节点上
render(vnode, container)
// 在页面上添加,即显示
document.body.appendChild(container.firstElementChild!)
- 用一个数组来保存已创建的组件实例
- 通过维护这个数组来进行组件的添加和删除
// 存储当前创建的实例,并且使用shallowReactive做浅层监听
const instances: MesssageContext[] = shallowReactive([])
const instance = {
id,
vnode,
props: newProps,
vm,
// manualDistory下文添加
destroy: manualDistory
}
// 将当前创建的实例添加到实例数组中
instances.push(instance)
// 手动调用删除, 也就是手动调整组件中的visible值
// visible 是通过expose传出来的
const manualDistory = () => {
const instance = instances.find((instance) => instance.id === id)
if (instance) {
instance.vm.exposed!.visible.value = false
}
}
// 卸载组件
const destroyMesssage = () => {
// 从实例数组中删除
const index = instances.findIndex((instance) => instance.id === id)
if (index === -1) return
instances.splice(index, 1)
render(null, container)
}
- 有多个Message被创建时,要设置每个组件的位置
- 计算偏移量:top= 上一个实例留下的底部偏移 + offset
- 为下一个实例预留底部偏移量:top + height
- 通过getBoundingClientRect().height拿到高度
- 将偏移量暴露出去
// 拿到最后一个组件实例
export const getLastInstance = () => {
return instances.at(-1)
}
// 获取上一个实例的offset
export const getLastBottomOffset = (id: string) => {
const index = instances.findIndex((instance) => instance.id === id)
// 如果 index <= 0 说明是第一项
if (index <= 0) {
return 0
} else {
const prev = instances[index - 1]
// 拿到上一项的bottomOffset
return prev.vm.exposed!.bottomOffset.value
}
}
Message组件
在组件中要做什么呢,通过之前的分析及部分实现,不难看出在组件中需要做的事情是比较简单:
- 定义属性并初始化
const props = withDefaults(defineProps<MessageProps>(), {
type: 'info',
duration: 3000,
offset: 20,
transitionName: 'fade-up'
})
- 添加实现组件显示隐藏的功能
const visible = ref<boolean>(false)
const handleClose = () => {
visible.value = false
}
- 确定组件位置
// 计算偏移高度
const height = ref(0)
// 上一个实例的最下面的坐标数字,第一个是0
const lastOffset = computed(() => getLastBottomOffset(props.id))
// 当前元素应该使用的top
const topOffset = computed(() => props.offset + lastOffset.value)
// 该元素为下一个元素预留的offset, 也就是它最低端bottom的值
const bottomOffset = computed(() => height.value + topOffset.value)
// 计算出组件样式
const cssStyle = computed(() => ({
top: topOffset.value + 'px',
zIndex: props.zIndex
}))
- 添加定时器,并且在合适的时机关闭组件
let timer: any;
const startTimer = () => {
if (props.duration === 0) return
timer = setTimeout(() => {
visible.value = false
}, props.duration)
}
const clearTimer = () => {
clearTimeout(timer)
}
- 在合适的时机更新组件
onMounted(async () => {
visible.value = true
startTimer()
})
function destroyComponent() {
props.useDestroy()
}
function updateHeight() {
height.value = messageRef.value!.getBoundingClientRect().height
}
- 暴露属性
defineExpose({
bottomOffset,
visible
})
最终的Message组件
<template>
<Transition :name="props.transitionName" @after-leave="destroyComponent" @enter="updateHeight">
<div class="yv-message" v-show="visible" role="alert" :class="{
[`yv-message--${type}`]: type,
'is-close': props.showClose
}" ref="messageRef" :style="cssStyle" @mouseenter="clearTimer" @mouseleave="startTimer">
<div class="yv-message__content">
<slot>
<RenderVnode :v-node="props.message" v-if="props.message" />
</slot>
</div>
<div class="yv-message__close" v-if="props.showClose">
<Icon icon="xmark" @click.stop="handleClose"></Icon>
</div>
</div>
</Transition>
</template>
其中after-leave
和enter
为Transition组件中的钩子函数,你可以通过钩子函数在过渡过程中自定义你想要的操作,详见 官方文档-Transition组件-JavaScript 钩子
总结
本文简单介绍了Message组件的基本实现,与其他组件不同的是,这里使用函数式的方式创建组件,并且可以创建多个组件并显示。写到最后自己都有点懵了,感觉逻辑不是很清晰了已经,之后会更新更加详细的逻辑,如有失误,感谢评论区批评指正。