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

最佳实践:

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

总结

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

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

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

具体实现请访问 zhang-glitch github

往期年度总结

往期文章

专栏文章

相关推荐
溪语流沙5 小时前
【每天一个CSS | Day18】会摆动的拟物怀表:黄铜、皮革与催眠摆
前端·css3·html5
OpsEye5 小时前
大模型会话里的手机号、身份证,怎么自动脱敏?
javascript·ai编程
Lvan的前端笔记5 小时前
docker:基于docker的前端部署方案
前端·docker·状态模式
程序人生8885 小时前
从“把隐私文件上传到别人云端“到本地可控:DocConverter Web 立项初心与架构选型
前端·图像处理·人工智能·opencv·架构·ocr·文心一言
开开心心就好5 小时前
视频里的图片怎么提取?双击一下就导出
java·前端·人工智能·spring·智能手机·intellij-idea·excel
爱喝水的小周5 小时前
实验十 Vue‑Router 王者荣耀战绩查询 APP 开发实践
前端·javascript·vue.js
yexianglunbai5 小时前
Vue 3 全面详解:从核心特性到工程化实践
前端·javascript·vue.js
传人once5 小时前
选择文件按钮样式如何美化
前端·html·css3·jquery·html5
parade岁月5 小时前
告别 Deno 时代?运行时进入一年倒计时,Deno 团队并入 Cloudflare
前端
Lucky_Turtle5 小时前
【HTML】前端页面禁止缩放
前端·html