Vue3+ts封装一个简单版的Message组件

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

javascript 复制代码
<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

javascript 复制代码
<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

javascript 复制代码
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-define
    if (appContext ?? Message.myContext) {
      // eslint-disable-next-line no-use-before-define
      vm.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-define
      messageInstance = 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
typescript 复制代码
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组件就完成了。如果有更好的实现方法,欢迎在评论区讨论。同时也欢迎各位大佬指出我的不足

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax