文章
努力加载图片中...
【Vue3】Pinia + Transition-Group 优雅实现 Message 消息组件
  • 1455 字

  • 2 分钟

  • 14 次

  • 2025-12-31
标签:

1 前言

消息组件是非常常用的组件之一,Element Plus 和 Naive UI 都有提供相应的消息组件,使用起来也相当方便。

那么,如果自己实现一个,怎么做呢?

我最初的方案, 是用 VNode 实现的,创建一个 Message 组件的 VNode ,然后在 body 渲染出来。

这种办法的缺点很明显,写起来很复杂,最主要的是,样式和过渡动画不好控制。

现在,我使用一个新的方案,可以非常优雅,非常简单的实现这个消息组件,并且可以很轻松的控制样式和动画效果。

话不多说,看效果 GIF 2025-12-31-星期三 16-10-45.gif

2 实现

思路相当简单。响应式的数据,就由 Pinia 来做,然后渲染就是用 transition-group + v-for 的形式,渲染 Pinia 的响应式数据,没了。

首先,先要知道我们的组件需要什么。也就是要先定义类型。

想必一下就能猜到了,首先,必要就是消息内容 message,然后就是消息的类型 type,包括 info、success、warning、error 这几种。

但如果要手动控制持续时间的话,就是使用 setTimeout 来做,就需要再加一个 duration 。

此外,如果我们要删除某个数据的话,还得用一个自动标识每一个数据,就需要 id 字段。

这样,最基本的数据就差不多定义好了。message.d.ts 的内容如下

ts
interface IMessageOption { message: string type?: 'success' | 'error' | 'warning' | 'info' duration?: number } interface IMessageInstanceItem extends IMessageOption { id: symbol // symbol 唯一且不可变 } export type { IMessageOption, IMessageInstanceItem }

为什么要分开定义?因为 id 是自动生成的,调用者不需要传,所以这里分开定义,方便写添加方法。当然,你要全部写一起也可以,这只是我的习惯。

现在就是要创建一个仓库,来存储全局响应式数据。

ts
import { defineStore } from 'pinia' import { ref } from 'vue' import type { IMessageInstanceItem, IMessageOption } from '@/components/types/message' export const useMessageStore = defineStore( 'message', () => { // 消息实例数组 const messageInstance = ref<IMessageInstanceItem[]>([]) function add(message: IMessageOption) { // 生成 id const id = Symbol() // 添加消息实例 messageInstance.value.push({ id: id, ...message }) // 定时关闭 setTimeout(() => { remove(id) }, message.duration ?? 3000) } function remove(id: symbol) { messageInstance.value = messageInstance.value.filter(item => item.id !== id) } return { messageInstance, add, remove } } )

然后,就是创建一个基础的消息组件。我就使用 tailwindcss + font awesome 快速整一个 BasicMessage.vue 组件。

vue
<script setup lang="ts"> import type { IMessageOption } from '@/components/types/message' withDefaults(defineProps<IMessageOption>(), { duration: 3000, type: 'info' }) const icons = { info: 'fas fa-circle-info', success: 'fas fa-circle-check', warning: 'fas fa-circle-exclamation', error: 'fas fa-circle-xmark', } </script> <template> <div class="shadow-xl rounded-md min-w-40 w-fit h-10 px-4 py-2 flex flex-row items-center gap-2 text-white" :class="[type]"> <i :class="[icons[type]]" /> <span>{{ message }}</span> </div> </template> <style scoped> @reference "@/assets/main.css"; .info { @apply bg-sky-400; } .success { @apply bg-green-400; } .warning { @apply bg-yellow-400; } .error { @apply bg-red-400; } </style>

非常简单的组件,如果要支持手动关闭的,defineProps 的泛型换成 IMessageInstanceItem 类型,也就是把 id 传到组件中,然后点击某个地方关闭的时候,根据 id 调用仓库的 remove 方法就行。

接下来,就是要解决渲染问题了。

要渲染,那肯定是要一个容器。容器放在哪?当然是根组件。并且,消息是始终在窗口顶部的,不受页面滚动影响,所以容器必须是 fixed 布局。

这样,思路就清晰了,定义一个 fixed 布局的父容器,挂到根组件,然后内部使用 v-for 渲染仓库中的实例数据,动画效果,就使用 transition-group 来控制。父容器组件:MessageProvider.vue

vue
<script setup lang="ts"> import BasicMessage from '@/components/message/BasicMessage.vue' import { useMessageStore } from '@/stores/messageStore.ts' const messageStore = useMessageStore() </script> <template> <div class="z-100 h-fit w-fit fixed top-4 left-1/2 -translate-x-1/2"> <transition-group name="message" tag="div" class="h-fit min-w-50 w-fit flex flex-col gap-4 items-center" > <basic-message v-for="instance in messageStore.messageInstance" :key="instance.id" :message="instance.message" :type="instance.type" :duration="instance.duration" /> </transition-group> </div> </template> <style scoped> @reference "@/assets/main.css"; .message-move, .message-enter-active, .message-leave-active { @apply transition-all duration-1000 ease-in-out; } .message-enter-from { @apply opacity-0 translate-y-10 scale-80; } .message-leave-to { @apply opacity-0 -translate-y-10 scale-80; } .message-leave-active { @apply absolute; } </style>

Transition-Group 的使用,可以参考官网:TransitionGroup | Vue.js,非常适合控制动态列表数据的过渡效果。

然后,把 MessageProvider.vue 挂到 App.vue 组件中

vue
<script setup lang="ts"> import MessageProvider from '@/components/message/MessageProvider.vue' </script> <template> <RouterView/> <message-provider /> </template> <style scoped></style>

完成了。

接下来就是写调用代码,只需要写一个非常简单的 hook 。

ts
import { useMessageStore } from '@/stores/messageStore.ts' const useMessage = () => { function info(message: string) { // 按需获取,防止获取时,pinia 未初始化。 const messageStore = useMessageStore() messageStore.add({ message: message, type: 'info' }) } function success(message: string) { const messageStore = useMessageStore() messageStore.add({ message: message, type: 'success' }) } function warning(message: string) { const messageStore = useMessageStore() messageStore.add({ message: message, type: 'warning' }) } function error(message: string) { const messageStore = useMessageStore() messageStore.add({ message: message, type: 'error' }) } return { info, success, warning, error } } export const message = useMessage()

App.vue 写一个简单的调用代码。

vue
<script setup lang="ts"> import MessageProvider from '@/components/message/MessageProvider.vue' import { message } from '@/components/message/hook/useMessage.ts' const init = async () => { await new Promise(resolve => { setTimeout(() => { message.info('This is a message') resolve(true) }, 1000) }) await new Promise(resolve => { setTimeout(() => { message.success('This is a message') resolve(true) }, 1000) }) await new Promise(resolve => { setTimeout(() => { message.warning('This is a message') resolve(true) }, 1000) }) await new Promise(resolve => { setTimeout(() => { message.error('This is a message') resolve(true) }, 1000) }) } init() </script> <template> <RouterView/> <message-provider /> </template> <style scoped></style>

效果如下 GIF 2025-12-31-星期三 16-10-45.gif

3 总结

  1. 学习了 Transition-Group 的用法。
  2. 学会了如何使用 Pinia + Transition-Group 的方式实现消息组件。
作者: Xigrut发布时间: 2025-12-31 17:04:05上次编辑时间: 2026-06-18 19:21:44 许可协议: CC BY-NC-SA 4.0
留言区