vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue3+ts封装Message组件

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

作者:实在想不出什么名字

近日项目中需要使用信息提示框的功能,ui组件库使用的是字节的arco-design-vue,看了一下,现有的Message不满足要是需求,直接使用message组件的话,改样式太麻烦,所以本文就本就介绍了基于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-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-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

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组件就完成了。如果有更好的实现方法,欢迎在评论区讨论。同时也欢迎各位大佬指出我的不足

以上就是基于Vue3+ts封装一个简单版的Message组件的详细内容,更多关于Vue3+ts封装Message组件的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文