Vue3+ts封装一个Message组件
项目中需要使用信息提示框的功能,ui组件库使用的是字节的arco-design-vue。看了一下,现有的Message不满足要是需求,直接使用message组件的话,改样式太麻烦。Notification组件样式倒是符合了,但是弹出的位置不符合,查看了一下相关api,这位置不支持"top"。既然如此,那就去查看它俩的源码,找到我想要的,修修改改,自己也写一个,嘻嘻。
源码分析
源码(以Message组件为例)主要分为三个模块:message-item.vue、message-list.vue、message.ts。下面简单介绍一下这三个模块
- message-item.vue: 主要是编写Message信息弹框的样式和弹框按时(定时)自动消失(关闭)的逻辑
- message-list.vue:主要就是添加动画
- message.ts:这个最主要的。这里面要实现创建message、刷新message、关闭message、销毁message以及message分类等一系类逻辑,同时还要把这些逻辑封装(暂时找不到更好的词来表达)成一个对象抛出去。没错,这是一个单例设计模式(全局永远只有一个Message对象)
代码实现
在完全实现之前,我看 message-list.vue 组件中的逻辑那么简单,想也不想就直接把它跟 message-item.vue 组成了一个组件 message-ul.vue,等到要实现逻辑的时候,才发现自己这个写不行,因为每一个Message都可以设置自己的持续时长,时间一到就自己关闭了,不会影响到其他的。但是在 message-ul.vue 组件中,处理的是一个列表,要保证每个列表项的定时关闭不影响其他的,我没想到解决办法(呜呜呜,我太笨了 ),所以最后还是老老实实跟 arco-design-vue 一样(其实就是直接copy它)把 message-ul 组件拆分。
ps:后来我想了想,那可是字节哎,里面的大佬这么写肯定是有原因的,我老老实实照着抄就行了,居然妄想组合操作。不过实践检验真理,我也算是…好吧,我就是瞎搞了。
不哔哔了,看代码吧!
message-item.vue
<template><li class="message-item message"><p class="title"><img v-if="type === 'success'" src="@/assets/icons/task-sucess.png" alt="" /><img v-if="type === 'error'" src="@/assets/icons/task-error.png" alt="" /><img v-if="type === 'warn'" src="@/assets/icons/task-cancel.png" alt="" /><span>{{ title }}</span></p><p v-if="prompt" class="name">{{ prompt.length >= 20 ? `${prompt.slice(0, 15)}...` : prompt }}</p><span class="close" @click="handleClose"><IconClose /></span></li>
</template><script lang="ts" setup>import { onMounted, onUnmounted } from 'vue';import { IconClose } from '@arco-design/web-vue/es/icon';const emits = defineEmits(['close']);let timer = 0; // 每个实例?(item)有自己的定时器,这样就不会影响其他的item了const props = defineProps({title: String, // 标题prompt: {type: String,default: undefined,},id: {type: [String, Number],required: true,},type: {type: String,required: true,},duration: {type: Number,default: 3000,},});const clearTimer = () => {if (timer) {window.clearTimeout(timer);timer = 0;}};function handleClose() {emits('close', props.id);}const startTimer = () => {if (props.duration > 0) {timer = window.setTimeout(handleClose, props.duration);}};onMounted(() => {startTimer();});onUnmounted(() => {clearTimer();});
</script>
message-list.vue
<template><TransitionGroup tag="ul" class="message-box" name="list" :theme="appStore.theme"><Message v-for="item in messages" :key="item.id" v-bind="item" @close="handleClose" /></TransitionGroup>
</template><script lang="ts" setup>import type { PropType } from 'vue';import { useAppStore } from '@/store';import { MessageItem } from './types';import Message from './message-item.vue';const appStore = useAppStore();const emits = defineEmits(['close']);defineProps({messages: {type: Array as PropType<MessageItem[]>,default: () => [],},});function handleClose(id: string | number) {emits('close', id);}
</script><style lang="less">// 样式这里就放个动画的,其他的就不放了.list-enter-active,.list-leave-active {transition: all 0.5s ease;}.list-enter-from {opacity: 0;transform: translateY(30px);}.list-leave-to {opacity: 0;transform: translateY(-30px);}
</style>
message.ts
import type { AppContext, Ref } from 'vue';
import { createVNode, render, ref, reactive } from 'vue';
import { MessageItem, MessageConfig, MessageMethod } from './types';import messageVue from './message-list.vue';type _MessageConfig = MessageConfig & {type: 'success' | 'error' | 'warn';
};class MessageManger {// 定义一个集合,用于存储所有message的id,这里使用Set是为了自动去重private readonly messageIds: Set<number | string>;// 定义一个响应式集合用于存储所有message对象,用响应式的好处,数据变试图变,试图变数据变private readonly messages: Ref<MessageItem[]>;private container: HTMLElement | null;private messageCount = 0;constructor(config: _MessageConfig, appContext?: AppContext) {this.messageIds = new Set();this.messages = ref([]);this.container = document.createElement('div');this.container.setAttribute('class', 'my-message');// 创建一个虚拟DOM:同时messages传递给 messageVue;处理(实现) messageVue 向外抛出的close、afterClose等方法,const vm = createVNode(messageVue, {messages: this.messages.value,onClose: this.remove,onAfterClose: this.destroy,});// eslint-disable-next-line no-use-before-defineif (appContext ?? Message.myContext) {// eslint-disable-next-line no-use-before-definevm.appContext = appContext ?? Message.myContext;}// 将虚拟DOM渲染到指定容器中,并将容器添加到body标签里render(vm, this.container);document.body.appendChild(this.container);}add = (config: _MessageConfig) => {// 添加message时,如果没有传递id就使用 messageCount 创建一个this.messageCount += 1;const id = config.id ?? `message_${this.messageCount}`;if (this.messageIds.has(id)) {return this.update(id, config);}const message: MessageItem = reactive({ id, ...config });this.messages.value.push(message);this.messageIds.add(id);return {close: () => this.remove(id),};};update = (id: number | string, config: _MessageConfig) => {for (let i = 0; i < this.messages.value.length; i += 1) {if (this.messages.value[i].id === id) {const resetOnUpdate = config.duration !== undefined;Object.assign(this.messages.value[i], { ...config, id, resetOnUpdate });break;}}return {close: () => this.remove(id),};};remove = (id: number | string) => {for (let i = 0; i < this.messages.value.length; i += 1) {const item = this.messages.value[i];if (item.id === id) {if (typeof item.onClose === 'function') {item.onClose(id);}this.messages.value.splice(i, 1);this.messageIds.delete(id);break;}}this.destroy();};clear = () => {this.messages.value.splice(0);};destroy = () => {// 如果所有message都关闭了,那就销毁整个实例对象,并从body中移除容器if (this.messages.value.length === 0 && this.container) {render(null, this.container);document.body.removeChild(this.container);this.container = null;// eslint-disable-next-line no-use-before-definemessageInstance = null;}};
}let messageInstance: MessageManger | null = null;const types = ['success', 'error', 'warn'] as const;
const message = types.reduce((pre, value) => {pre[value] = (config: MessageConfig, appContext?: AppContext) => {const newConfig: _MessageConfig = { ...config, type: value };if (!messageInstance) {messageInstance = new MessageManger(newConfig, appContext);}return messageInstance.add(newConfig);};return pre;
}, {} as MessageMethod);message.clear = () => {if (messageInstance) {messageInstance?.clear();}
};
const Message = {...message,myContext: null as AppContext | null,
};
// 上面这段代码等同于
/* const message = {success: (config: MessageConfig, appContext?: AppContext) => {const newConfig: _MessageConfig = { ...config, type: 'success' };if (!messageInstance) {messageInstance = new MessageManger(newConfig, appContext);}return messageInstance.add(newConfig);},error: (config: MessageConfig, appContext?: AppContext) => {const newConfig: _MessageConfig = { ...config, type: 'error' };if (!messageInstance) {messageInstance = new MessageManger(newConfig, appContext);}return messageInstance.add(newConfig);},warn: (config: MessageConfig, appContext?: AppContext) => {const newConfig: _MessageConfig = { ...config, type: 'warn' };if (!messageInstance) {messageInstance = new MessageManger(newConfig, appContext);}return messageInstance.add(newConfig);},clear: () => {if (messageInstance) {messageInstance?.clear();}},
}; */// 抛出去的是一个对象,每次调用 message.xxx() 都是调用的同一个对象,所以这是一个单例模式
export default message;
types.ts
import type { AppContext } from 'vue';export interface MessageItem {id: number | string;title: string;prompt?: string;type: 'success' | 'error' | 'warn';duration?: number;closable?: boolean;onClose?: (id: number | string) => void;
}export interface MessageConfig {prompt?: string;title: string;id?: string;closable?: boolean;duration?: number;onClose?: (id: number | string) => void;type?: 'success' | 'error' | 'warn';
}export interface MessageReturn {close: () => void;
}export interface MessageMethod {success: (config: MessageConfig, appContext?: AppContext) => MessageReturn;error: (config: MessageConfig, appContext?: AppContext) => MessageReturn;warn: (config: MessageConfig, appContext?: AppContext) => MessageReturn;remove: (id: string) => void;clear: () => void;
}
到此,一个使用Vue3+TS实现的简单版的Message组件就完成了。如果有更好的实现方法,欢迎在评论区讨论。同时也欢迎各位大佬指出我的不足