概述
Message 组件是前端 UI 库中常见的提示组件,用于向用户提供即时反馈信息。本文将详细分析一个基于 Vue 3 的 Message 组件实现,涵盖组件设计、类型定义、实例管理等关键细节。这个组件不仅实现了基本的提示功能,还解决了多实例堆叠、动态布局、内存管理等复杂问题。
类型系统设计
在 types.ts 中定义了完整的类型系统:
typescript
export interface MessageProps {
message: string | VNode; // 支持字符串和VNode内容
type?: 'success' | 'info' | 'warning' | 'danger'; // 消息类型
duration?: number; // 显示持续时间
offset?: number; // 偏移距离
showClose?: boolean; // 是否显示关闭按钮
showIcon?: boolean; // 是否显示图标
id?: string; // 唯一标识
onDestory: () => void // 销毁回调
}
设计亮点:
- 支持字符串和 VNode 两种内容类型,提高组件灵活性
- 使用
Omit<MessageProps, 'id' | "onDestory">创建CreateMessageProps类型,自动排除内部管理的属性 - 严格遵循项目规范,确保必传属性不被遗漏
核心组件实现
1. Message.vue 组件
属性定义和默认值
typescript
const props = withDefaults(defineProps<MessageProps>(), {
type: 'info',
duration: 3000,
showIcon: true,
offset: 16
})
图标映射系统
typescript
const iconMap = {
primary: 'circle-info',
success: 'circle-check',
warning: 'circle-exclamation',
danger: 'triangle-exclamation',
info: 'circle-info'
}
设计细节:
- 图标映射确保不同类型的消息有对应的视觉标识
- 使用 Font Awesome 图标系统,符合项目技术选型
定时器管理
typescript
let timer: NodeJS.Timeout
function startTimer() {
if (props.duration === 0) return
timer = setTimeout(() => {
visible.value = false
props.onDestory()
}, props.duration)
}
function clearTimer() {
clearTimeout(timer)
}
交互细节:
- 鼠标悬停时暂停自动关闭 (
@mouseenter="clearTimer") - 鼠标离开时重新开始计时 (
@mouseleave="startTimer") duration为 0 时禁用自动关闭功能
组件暴露接口
typescript
defineExpose({
close: () => {
visible.value = false
}
})
关键点:
- 通过
defineExpose暴露close方法,允许外部手动关闭消息 - 符合 Vue 3 的封装原则,只暴露必要的接口
实例管理机制
1. 全局实例管理
在 index.ts中实现了完整的实例管理系统:
typescript
interface MessageInstance {
id: string,
el: HTMLElement,
instance: InstanceType<typeof Message>,
offset: number
}
设计细节:
- 每个实例包含 ID、DOM 元素、组件实例和计算出的偏移量
2. 唯一ID生成
typescript
let index = 1
const messageInstances: MessageInstance[] = []
export default function HmMessage(props: CreateMessageProps): InstanceType<typeof Message> {
const id = `message_${index++}`
// ...
}
实现细节:
- 使用递增索引确保每个消息实例有唯一ID
- ID用于实例查找和管理,避免冲突
3. 动态渲染策略
typescript
render(h(Message, {
...props, id,
offset,
onVnodeMounted: (vnode: VNode) => {
componentInstance = vnode.component;
},
onDestory: close
}), container)
nextTick(() => {
const el = container.firstElementChild as HTMLElement
el && document.body.appendChild(el)
// 添加到实例数组
})
技术细节:
- 使用
render函数动态渲染组件,符合 Vue 3 的渲染 API - 通过
onVnodeMounted钩子获取组件实例 nextTick确保 DOM 渲染完成后再添加到 body
4. 垂直排列算法
calculateOffset函数实现了智能的垂直排列:
typescript
function calculateOffset(baseOffset, lastMessage?: MessageInstance, delFirstEl = false) {
// 销毁计算时,第一个元素特殊处理
if (messageInstances.length === 0 || (delFirstEl && !lastMessage)) {
return baseOffset;
}
lastMessage = lastMessage || messageInstances[messageInstances.length - 1];
const lastElement = lastMessage.el;
const lastHeight = lastElement.offsetHeight;
const lastOffset = lastMessage.offset;
return lastOffset + lastHeight + baseOffset;
}
算法逻辑:
- 获取前一个消息的高度和位置
- 使用
offsetHeight获取实际渲染高度,提升布局准确性 - 计算新消息的垂直偏移量,确保消息之间有适当的间距
关闭和销毁机制
1. 实例移除逻辑
typescript
const close = () => {
const index = messageInstances.findIndex(item => item.id === id)
if (index > -1) {
messageInstances.splice(index, 1)
}
componentInstance?.exposed?.close?.()
// 重新计算后续组件的offset
let preIndex = index
messageInstances.slice(index).forEach((item) => {
item.offset = calculateOffset(props.offset || 16, messageInstances[preIndex - 1], true)
item.el.style.top = `${item.offset}px`
preIndex++
})
}
销毁细节:
- 从实例数组中移除当前消息
- 调用组件的
close方法触发动画 - 重新计算并更新后续消息的位置,避免出现跳跃
- 使用
slice(index)获取所有后续实例,确保布局紧凑无空隙
样式和动画
过渡动画
vue
<Transition name="fade-up">
<div class="hm-message" v-if="visible">
<!-- 消息内容 -->
</div>
</Transition>
动画细节:
- 使用
fade-up过渡效果,提供平滑的显示/隐藏体验 - CSS 类名遵循 BEM 规范
- 动画定义在全局样式中,确保一致性
内容渲染策略
VNode 支持
vue
<slot>
<RenderVNode :vnode="message" v-if="message" />
</slot>
实现细节:
- 使用
RenderVNode组件渲染 VNode - 支持复杂内容的动态渲染
- 同时兼容字符串和 VNode 两种内容类型
使用方式
typescript
// 创建消息
const message = HmMessage({
message: '操作成功',
type: 'success',
duration: 3000,
showClose: true
})
// 手动关闭
message.close()
最佳实践:
- 必须传入
message和type属性 - 可通过返回的实例对象手动关闭消息
总结
这个 Message 组件的实现展现了现代 Vue 组件开发的多个最佳实践:
- 类型安全:完整的 TypeScript 类型定义,严格遵循项目规范
- 实例管理:智能的全局实例管理系统,解决多实例堆叠问题
- 交互体验:鼠标悬停暂停、平滑动画、可配置的关闭机制
- 布局算法:基于实际渲染高度的动态垂直排列计算
- 内存管理:正确的销毁和清理机制,避免内存泄漏
- 内容灵活性:同时支持字符串和 VNode 内容渲染
整个实现充分考虑了用户体验、性能优化和代码可维护性,是一个高质量的组件实现。它不仅解决了基础的提示需求,还通过精心设计的实例管理和布局算法,提供了专业级的用户体验,完全符合 hm-ui 组件库的设计理念和技术规范。
具体实现请访问 zhang-glitch github
往期年度总结
往期文章
- 展示大量数据节点(tree),引发的一次性能排查
- ts装饰器的那点东西
- 这是你所知道的ts类型断言和类型守卫吗?
- TypeScript官网内容解读
- 经常使用ts的你,知道这些内容?
- 你有了解过原生css的scope?
- 现在比较常用的移动端调试你知道哪些?
- 众多跨标签页通信方式,你知道哪些?(二)
- 众多跨标签页通信方式,你知道哪些?
- 反调试吗?如何监听devtools的打开与关闭
- 因为原生,选择一家公司(前端如何防笔试作弊)
- 结合开发,带你熟悉package.json与tsconfig.json配置
- 如何优雅的在项目中使用echarts
- 如何优雅的做项目国际化
- 近三个月的排错,原来的憧憬消失喽
- 带你从0开始了解vue3核心(运行时)
- 带你从0开始了解vue3核心(computed, watch)
- 带你从0开始了解vue3核心(响应式)
- 3w+字的后台管理通用功能解决方案送给你
- 入职之前,狂补技术,4w字的前端技术解决方案送给你(vue3 + vite )