vue3实现一个Toast组件

528 阅读3分钟

vuetoast.png

如果我们的Vue项目中没有用到任何UI框架的话,为了更好的用户体验,肯定会用到toast。那么我们就自定义这个组件吧。

在 src/components下创建toast文件夹,并依此创建index.vue和index.ts

创建模版index.vue

一般toast会有如下功能:背景色、字体颜色、文本。停留时间。

<template>
<div class="toast-box" >
    <p class="toast-value" :style="{background: background, color: color}">
        {{ value }}
    </p> 
</div>
</template>    
<script lang="ts">
    import { defineComponent } from 'vue'
    export default defineComponent({
        name: 'Toast',
        props: {
            value: {
                type: String,
                default: ''
            },
            duration: {
                type: Number,
                default: 3000
            },
            background: {
                type: String,
                default: '#000'
            },
            color: {
                type: String,
                default: '#fff'
            }
        }
    })
</script>

<style>
.toast-box  {
    position: fixed;
    width: 100vw;
    height: 100vh;
    top: 0;
    left: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}
.toast-value {
    max-width: 100px;
    background: rgb(8, 8, 8);
    padding: 8px 10px;
    border-radius: 4px;
    text-align: center;
    display: inline-block;
    animation: anim 0.5s;
}
@keyframes anim { 
    0% {opacity: 0;}
    100%{opacity:1;}
}
.toast-value.reomve {
    animation: reomve 0.5s;
}
@keyframes reomve { 
    0% {opacity: 1;}
    100%{opacity:0;}
}
</style>

导出Toast方法

  • 创建时

1.首先使用createVNode方法创建一个vNode独享;2.使用render方法转换成真实dom;3.添加到body上。

  • 销毁时

1.首先添加一个淡入淡出效果;2.使用render将真实设置为null;3.移除创建的dom;

import { createVNode, render } from 'vue'
import toastTemplate from './index.vue'
export interface IProps {
    value?: string;
    duration?: number;
    background?: string;
    color?: string;
}
const defaultOpt = { // 创建默认参数
    duration: 3000
}

export interface ResultParams {
    destory?: () => void;
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
const Toast = (options: IProps):ResultParams => {
    const container = document.createElement('div')
    const opt = {...defaultOpt,...options}
    const vm = createVNode(toastTemplate, opt) // 创建vNode
    render(vm, container)
    document.body.appendChild(container)       // 添加到body上
    const destory =  ()=> {
        const dom = vm.el as HTMLDivElement
        if(dom.querySelector('.toast-value')) {
            dom.querySelector('.toast-value')?.classList.add('reomve') // 销毁时添加淡入淡出效果
            const t = setTimeout(() => {             // 淡入淡出效果之后删除dom节点
                render(null, container)
                document.body.removeChild(container)
                clearTimeout(t)
            },500);
        } 
    }
    if(opt.duration) {                            // 如果传入的值为0可以持续保留在页面,需要手动销毁
        const timer = setTimeout(()=> {
            destory()
            clearTimeout(timer)
        }, opt.duration)
    }
    return {
        destory
    }
}
export default Toast

测试

<template>
  <div class="home">
   测试toast
  </div>
</template>

<script lang="ts">
import { defineComponent, onMounted } from 'vue';
import Toast from '@/components/toast'; // @ is an alias to /src

export default defineComponent({
  name: 'Home',
  setup() {
    onMounted(()=> {
      const toast = Toast({
        value: 'toast',
        duration: 0, // 如果大于0则不必使用destory方法
        background: '#000',
        color: '#fff'
      })
      setTimeout(() => {
        toast.destory && toast.destory()
      }, 3000);
     
    })
    
  }
});
</script>

另一种写法,纯ts实现

import { provide, inject, reactive, createApp, h } from "vue";
const ToastSymbol = Symbol();

const globalConfig: any = {
  duration: 2500
};

const state = reactive({
  show: false, // toast元素是否显示
  text: ""
});

let toastTimer: any = null,
  toastVM: any = null,
  toastWrapper: any = null;

const _toast = (text: string) => {
  state.show = true;
  state.text = text;
  if (!toastVM) {
    // 如果toast实例存在则不重新创建
    toastVM = createApp({
      setup() {
        return () =>
          h(
            "span",
            {
              class: ["dialog-tips"],
              style: {
                display: state.show ? "block" : "none"
              }
            },
            state.text
          );
      }
    });
  }

  if (!toastWrapper) {
    // 如果该节点以经存在则不重新创建
    toastWrapper = document.createElement("div");
    toastWrapper.id = "lx-toast";
    document.body.appendChild(toastWrapper);
    toastVM.mount("#lx-toast");
  }
  if (toastTimer) clearTimeout(toastTimer);
  // 定时器,持续时长之后隐藏
  toastTimer = setTimeout(() => {
    state.show = false;
    clearTimeout(toastTimer);
  }, globalConfig.duration);
};

export function provideToast(config: any = {}) {
  for (const key in config) {
    globalConfig[key] = config[key];
  }
  provide(ToastSymbol, _toast);
}

export function useToast() {
  const toast = inject(ToastSymbol);
  if (!toast) {
    throw new Error("error");
  }
  return toast;
}

let mNowToast: any = null;
export function setToast(mObj: any) {
  mNowToast = mObj;
}
export function nowToast(mStr: string) {
  if (mNowToast) mNowToast(mStr);
}

//全局定义样式//////////////////////////////////////
/* 提示 */
.dialog-tips {
  position: fixed;
  z-index: 2000;
  left: 50%;
  top: 45%;
  transition: all 0.5s;
  -webkit-transform: translateX(-50%) translateY(-50%);
  -moz-transform: translateX(-50%) translateY(-50%);
  -ms-transform: translateX(-50%) translateY(-50%);
  -o-transform: translateX(-50%) translateY(-50%);
  transform: translateX(-50%) translateY(-50%);
  text-align: center;
  border-radius: 5px;
  color: #ffffff;
  background: rgba(17, 17, 17, 0.6);
  min-height: 100px;
  font-size: 26px;
  line-height: 40px;
  padding: 30px 40px;
  max-width: 500px;
}

//引用///////////////////////////////
//app.vue下设置
provideToast({
  duration: 2500
});
const Toast = useToast();
setToast(Toast);
//界面中使用
const Toast = useToast();
Toast("Hello World");