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对象)

image.png

代码实现

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/98967.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用maven创建springboot项目

创建maven快速启动项目 命令行或者idea、eclipse快捷创建也可以 pom.xml下project项目下导入springboot 父工程 <!--导入springboot 父工程--> <parent><artifactId>spring-boot-starter-parent</artifactId><groupId>org.springframework.bo…

Flink+Paimon多流拼接性能优化实战

目录 &#xff08;零&#xff09;本文简介 &#xff08;一&#xff09;背景 &#xff08;二&#xff09;探索梳理过程 &#xff08;三&#xff09;源码改造 &#xff08;四&#xff09;修改效果 1、JOB状态 2、Level5的dataFile总大小 3、数据延迟 &#xff08;五&…

研华I/O板卡 Win10+Qt+Cmake 开发环境搭建

文章目录 一.研华I/O板卡 Win10QtCmake 开发环境搭建 一.研华I/O板卡 Win10QtCmake 开发环境搭建 参考这个链接安装研华I/O板卡驱动程序系统环境变量添加研华板卡dll Qt新建一个c项目 cmakeList.txt中添加研华库文件 cmake_minimum_required(VERSION 3.5)project(advantechDA…

科技资讯|苹果发布新专利:可在车内定位苹果的智能设备

根据美国商标和专利局近期公示的清单&#xff0c;苹果公司获得了一项名为《车内定位移动设备的系统和方式》专利&#xff0c;概述了在车内狭窄空间内如何定位 iPhone 等移动设备。 Find My 服务现阶段没有使用 UWB 来追踪 iPhone 或者 iPad&#xff0c;而是依赖 GPS 等相关辅…

【Java并发】聊聊对象内存布局和syn锁升级过程

对象存储解析&#xff1a;一个空Object对象到底占据多少内存&#xff1f; 对象内存布局 Mark Word占用8字节&#xff0c;类型指针占用8个字节&#xff0c;对象头占用16个字节。 好了&#xff0c;我们来看一下一个Object对占用多少空间&#xff0c; 因为java默认是开启压缩…

Spring框架知识点汇总

01.Spring框架的基本理解 关键字&#xff1a;核心思想IOC/AOP&#xff0c;作用&#xff08;解耦&#xff0c;简化&#xff09;&#xff0c;简单描述框架组成&#xff1b; Spring框架是一款轻量级的开发框架&#xff0c;核心思想是IOC&#xff08;反转控制&#xff09;和AOP&a…

【C++】输入输出及格式控制

在各类算法竞赛和机试中&#xff0c;对测试数据和输出格式往往会有明确的规定&#xff0c;笔者结合个人刷题经历&#xff0c;得到了以下C语言输入输出控制的方法。 cin&#xff1a;从缓冲区中读取数据 cin>>从缓冲区中读取数据时&#xff0c;若缓冲区中第一个字符是空格…

DBeaver 23.1.5 发布

导读DBeaver 是一个免费开源的通用数据库工具&#xff0c;适用于开发人员和数据库管理员。DBeaver 23.1.5 现已发布&#xff0c;更新内容如下. Data editor 重新设计了词典查看器面板 UI 空间数据类型&#xff1a;曲线几何线性化已修复 数据保存时结果选项卡关闭的问题已解决…

2第一个Java程序

目录 1第一个Java代码 2类class 3运行Java文件 1第一个Java代码 public class Hello {public static void main(String[] args) {System.out.println("Hello, world!");} } 2类class public class Hello {public static void main(String[] args) {System.ou…

Win11搭建 Elasticsearch 7 集群(一)

一&#xff1a; ES与JDK版本匹配一览表 elasticsearch从7.0开始默认安装了java运行环境&#xff0c;以便在没有安装java运行环境的机器上运行。如果配置了环境变量JAVA_HOME&#xff0c;则elasticsearh启动时会使用JAVA_HOME作为java路径&#xff0c;否则使用elasticsearch根目…

以udp协议创建通信服务器

概念图 创建服务器让A,B主机完成通信。 认识接口 socket 返回值&#xff1a;套接字&#xff0c;你可以认为类似fd 参数&#xff1a; domain->:哪种套接字&#xff0c;常用AF_INET(网络套接字)、AF_LOCAL(本地套接字)type->&#xff1a;发送数据类型&#xff0c;常用 …

【校招VIP】校招考点之前端安全和注入

考点介绍&#xff1a; 随着前端的快速发展&#xff0c;各种技术不断更新&#xff0c;前端的安全问题也越来越值得我们重视。千万不要等到项目上线之后才去重视安全问题&#xff0c;到时候被黑客攻击一切都太晚了。今天的专题将讲述前端几大常见安全问题&#xff0c;在校招面试中…

【mysql】MySQL服务无法启动 NET HELPMSG 3534

MySQL服务无法启动 NET HELPMSG 3534 错误描述寻找原因解决方法 错误描述 mysql版本&#xff1a;8.1.0 mysql安装成功之后&#xff0c;使用net start mysql来启动mysql&#xff0c;然后出现了报错 MySQL服务无法启动 NET HELPMSG 3534 寻找原因 1、在cmd中&#xff0c;进入…

OpenCV: cv2.findContours - ValueError: too many values to unpack

OpenCV找轮廓findContours报错 ValueError: not enough values to unpack (expected 3,got 2) 问题指向这行代码&#x1f447; binary, cnts, hierarchy cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) 报错的意思是需要3个返回值但只给了两…

RabbitMQ工作模式-路由模式

官方文档参考&#xff1a;https://www.rabbitmq.com/tutorials/tutorial-four-python.html 使用direct类型的Exchange,发N条消息并使用不同的routingKey,消费者定义队列并将队列routingKey、Exchange绑定。此时使用direct模式Exchange必须要routingKey完成匹配的情况下消息才…

CSP的理解与绕过

文章目录 前言CSP简介CSP如何工作CSP指令CSP指令值 例题[AFCTF 2021]BABY_CSP 前言 刚学习完xss&#xff0c;把xsss-labs靶场都通了打算试试水&#xff0c;遇到此题[AFCTF 2021]BABY_CSP&#xff0c;借此机会学习下CSP CSP简介 Content Security Policy (CSP)内容安全策略&am…

【广州华锐互动】AR昆虫认知学习系统实现对昆虫形态的捕捉和还原

随着科技的不断发展&#xff0c;人们对自然界的认识也在不断加深。在这个过程中&#xff0c;AR&#xff08;增强现实&#xff09;技术的出现为人们带来了全新的体验方式。为此&#xff0c;广州华锐互动开发了AR昆虫认知学习系统&#xff0c;本文将为大家详细介绍这款系统的特点…

MinIO分布式存储k8s集群部署

一、MinIO是什么 MinIO是go开发的&#xff0c;高性能分布式存储&#xff1b;基于GNU AGPL v3开源&#xff0c;可免费使用&#xff1b; 官网&#xff1a;https://min.io/ github: https://github.com/minio/minio 官网宣传MinIO是世界上速度最快的分布式对象存储&#xff1b; …

【数据结构】顺序表详解

当我们写完通讯录后&#xff0c;顺序表肯定难不倒你&#xff0c;跟着小张一起来学习顺序表吧&#xff01; 线性表 线性表&#xff08;linear list&#xff09;是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构&#xff0c;常见的线性表&#x…

【SpringCloud】SpringCloud整合openFeign

文章目录 前言1. 问题分析2. 了解Feign3. 项目整合Feign3.1 引入依赖3.2 添加注解3.3 编写Feign客户端3.4 测试3.5 总结 4. 自定义配置4.1 配置文件方式4.2 Java代码方式 5. Feign使用优化5.1 引入依赖5.2 配置连接池 6. Feign最佳实践6.1 继承方式6.2 抽取方式 前言 微服务远…