
【Vue3】Pinia + Transition-Group 优雅实现 Message 消息组件
1 前言
消息组件是非常常用的组件之一,Element Plus 和 Naive UI 都有提供相应的消息组件,使用起来也相当方便。
那么,如果自己实现一个,怎么做呢?
我最初的方案, 是用 VNode 实现的,创建一个 Message 组件的 VNode ,然后在 body 渲染出来。
这种办法的缺点很明显,写起来很复杂,最主要的是,样式和过渡动画不好控制。
现在,我使用一个新的方案,可以非常优雅,非常简单的实现这个消息组件,并且可以很轻松的控制样式和动画效果。
话不多说,看效果

2 实现
思路相当简单。响应式的数据,就由 Pinia 来做,然后渲染就是用 transition-group + v-for 的形式,渲染 Pinia 的响应式数据,没了。
首先,先要知道我们的组件需要什么。也就是要先定义类型。
想必一下就能猜到了,首先,必要就是消息内容 message,然后就是消息的类型 type,包括 info、success、warning、error 这几种。
但如果要手动控制持续时间的话,就是使用 setTimeout 来做,就需要再加一个 duration 。
此外,如果我们要删除某个数据的话,还得用一个自动标识每一个数据,就需要 id 字段。
这样,最基本的数据就差不多定义好了。message.d.ts 的内容如下
interface IMessageOption {
message: string
type?: 'success' | 'error' | 'warning' | 'info'
duration?: number
}
interface IMessageInstanceItem extends IMessageOption {
id: symbol // symbol 唯一且不可变
}
export type {
IMessageOption,
IMessageInstanceItem
}
为什么要分开定义?因为 id 是自动生成的,调用者不需要传,所以这里分开定义,方便写添加方法。当然,你要全部写一起也可以,这只是我的习惯。
现在就是要创建一个仓库,来存储全局响应式数据。
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 组件。
<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
<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 组件中
<script setup lang="ts">
import MessageProvider from '@/components/message/MessageProvider.vue'
</script>
<template>
<RouterView/>
<message-provider />
</template>
<style scoped></style>
完成了。
接下来就是写调用代码,只需要写一个非常简单的 hook 。
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 写一个简单的调用代码。
<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>
效果如下

3 总结
- 学习了 Transition-Group 的用法。
- 学会了如何使用 Pinia + Transition-Group 的方式实现消息组件。
0 人赞过
评论
评论加载中…