Vue 3 Message 组件深度解析:从设计到实现的完整指南

概述

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()

最佳实践

  • 必须传入 messagetype 属性
  • 可通过返回的实例对象手动关闭消息

总结

这个 Message 组件的实现展现了现代 Vue 组件开发的多个最佳实践:

  1. 类型安全:完整的 TypeScript 类型定义,严格遵循项目规范
  2. 实例管理:智能的全局实例管理系统,解决多实例堆叠问题
  3. 交互体验:鼠标悬停暂停、平滑动画、可配置的关闭机制
  4. 布局算法:基于实际渲染高度的动态垂直排列计算
  5. 内存管理:正确的销毁和清理机制,避免内存泄漏
  6. 内容灵活性:同时支持字符串和 VNode 内容渲染

整个实现充分考虑了用户体验、性能优化和代码可维护性,是一个高质量的组件实现。它不仅解决了基础的提示需求,还通过精心设计的实例管理和布局算法,提供了专业级的用户体验,完全符合 hm-ui 组件库的设计理念和技术规范。

具体实现请访问 zhang-glitch github

往期年度总结

往期文章

专栏文章

相关推荐
C_心欲无痕12 小时前
网络相关 - http1.1 与 http2
前端·网络
一只爱吃糖的小羊12 小时前
Web Worker 性能优化实战:将计算密集型逻辑从主线程剥离的正确姿势
前端·性能优化
低保和光头哪个先来12 小时前
源码篇 实例方法
前端·javascript·vue.js
你真的可爱呀12 小时前
自定义颜色选择功能
开发语言·前端·javascript
小王和八蛋12 小时前
JS中 escape urlencodeComponent urlencode 区别
前端·javascript
奔跑的web.12 小时前
TypeScript类型系统核心速通:从基础到常用复合类型包装类
开发语言·前端·javascript·typescript·vue
Misnice12 小时前
Webpack、Vite 、Rsbuild 区别
前端·webpack·node.js
Kagol12 小时前
🎉历时1年,TinyEditor v4.0 正式发布!
前端·typescript·开源
丶一派胡言丶12 小时前
02-VUE介绍和指令
前端·javascript·vue.js
C_心欲无痕12 小时前
网络相关 - 跨域解决方式
前端·网络