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

往期年度总结

往期文章

专栏文章

相关推荐
奔跑的web.8 小时前
TypeScript 装饰器入门核心用法
前端·javascript·vue.js·typescript
阿蒙Amon8 小时前
TypeScript学习-第1章:入门
javascript·学习·typescript
winfredzhang8 小时前
实战复盘:如何用 HTML+JS+AI 打造一款“影迹”智能影视管理系统
javascript·html·json·加载·搜索·保存·电影接口
集成显卡8 小时前
Lucide Icons:一套现代、轻量且可定制的 SVG 图标库
前端·ui·图标库·lucide
pas1369 小时前
37-mini-vue 解析插值
前端·javascript·vue.js
十里-10 小时前
vue.js 2前端开发的项目通过electron打包成exe
前端·vue.js·electron
雨季66610 小时前
构建 OpenHarmony 简易文字行数统计器:用字符串分割实现纯文本结构感知
开发语言·前端·javascript·flutter·ui·dart
雨季66610 小时前
Flutter 三端应用实战:OpenHarmony 简易倒序文本查看器开发指南
开发语言·javascript·flutter·ui
小北方城市网10 小时前
Redis 分布式锁高可用实现:从原理到生产级落地
java·前端·javascript·spring boot·redis·分布式·wpf
console.log('npc')11 小时前
vue2 使用高德接口查询天气
前端·vue.js