Vue3 TypeScript 全局 Message 提示框

2021-10-08 11:20:46 浏览数 (1)

前言

实现过一个 vue2 ts 下的全局的 Message 提示框。闲暇之余想在 vue3 ts 的框架下也实现此功能。

技术思路

在 vue3 上的实现思路

install 函数是把编写的 Message 组件实例化并渲染到页面的关键。其中的步骤:

  1. 根据 Message 组件实例化一个 Message
  2. 在 document.body 上 appendChild 实例
  3. 同时返回一个 destory 函数用于手动销毁实例
  4. 倒计时时间到销毁实例

编写 Message 组件代码

代码语言:javascript复制
// Mssage.vue
<template>
  <transition name="slide">
    <div class="message-wrap" :class="[type, center ? 'text-center' : '']" :style="{ ...style }" v-if="visible">
      <div v-if="messageArr.length" class="message-line">
        <div v-for="(item, index) in messageArr" :key="index" class="message-line-item">
          {{ item }}
        </div>
      </div>
      <div v-else class="message">{{ message }}</div>
    </div>
  </transition>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
  props: {
    message: {
      type: String,
      default: '',
    },
    type: {
      type: String,
      default: 'success',
    },
    duration: {
      type: Number,
      default: 2000,
    },
  },
  data() {
    const messageArr: Array<string> = [];
    const style = {};
    return {
      messageArr,
      visible: true,
      center: false,
      style,
    };
  },
  created(): void {
    const arr = this.message.split('n');
    if (arr.length > 1) {
      this.messageArr = arr;
    }
  },
  mounted(): void {
    this.startTimer();
  },
  methods: {
    startTimer(): void {
      const { duration } = this;
      const timer = setTimeout(() => {
        this.visible = false;
        clearTimeout(timer);
      }, duration);
    },
  },
});
</script>
复制代码

编写 install 函数并挂载到 vue 全局

代码语言:javascript复制
// index.ts
import { App, render, createVNode } from 'vue';
import Message from './Message.vue';

const defaultOpt = {
  // 创建默认参数
  duration: 2000,
  type: 'success',
};

// 消息数组
const stack: Array<HTMLDivElement> = [];

/**
 * @description: 销毁 body 上的 Message 实例
 * @param {HTMLDivElement} ele
 * @return {*}
 */
const removeContainer = (ele: HTMLDivElement): void => {
  const index = stack.findIndex((item) => item === ele);
  if (~index) {
    stack.splice(index, 1);
    setStyle();
  }
};
/**
 * @description: 把实例加到实例队列 stack 中,并设置队列中搜有实例的 style
 * @param {HTMLDivElement} ele
 * @return {*}
 */
const addContainer = (ele: HTMLDivElement): void => {
  stack.push(ele);
  setStyle();
};
/**
 * @description: 设置 stack 中所有实例的 style
 * @param {*}
 * @return {*}
 */
const setStyle = () => {
  stack.forEach((item, index) => {
    if (item?.getElementsByClassName('message-wrap')?.[0]) {
      let top = 0;
      if (index > 0) {
        top  =
          (stack[index - 1].getElementsByClassName('message-wrap')[0] as HTMLElement)?.getBoundingClientRect()
            ?.bottom || 0;
      }
      // eslint-disable-next-line
      (item.getElementsByClassName('message-wrap')[0] as HTMLElement).style.marginTop = `${top}px`;
    }
  });
};

// 创建挂载实例
// eslint-disable-next-line
const createMount = (opts: { [key: string]: any }) => {
  const { duration } = opts;
  // 创建一个 div 容器
  const container = document.createElement('div');
  // 创建 Message 实例,createVNode 的性能比 h 更好
  const vm = createVNode(Message, opts);
  // 把实例 render 到容器上
  render(vm, container);
  addContainer(container);
  // 把容器渲染到 body 上
  document.body.appendChild(container);
  const destory = () => {
    const timer = setTimeout(() => {
      render(null, container);
      removeContainer(container);
      document.body.removeChild(container);
      clearTimeout(timer);
    }, 500); // 500 为动画结束时间,根据情况修改
  };

  const timer = setTimeout(() => {
    destory();
    clearTimeout(timer);
  }, duration || defaultOpt.duration);

  return { destory };
};

function Toast(options: { message: string; duration?: number } | string): {
  destory: () => void;
} {
  if (typeof options === 'string') {
    // eslint-disable-next-line
    options = {
      ...defaultOpt,
      message: options || '',
    };
  } else {
    // eslint-disable-next-line
    options = {
      ...defaultOpt,
      ...options,
    };
  }
  return createMount(options);
}

Toast.install = (app: App<Element>) => {
  app.component('toast', Message);
  app.provide('Toast', Toast);
  // 挂载 Toast 为全局方法 $toast
  // eslint-disable-next-line
  app.config.globalProperties.$toast = Toast;
};

export default Toast;
复制代码

use 为全局组件;增加 $toast 声明,消除使用时的 ts 报错

代码语言:javascript复制
import { createApp } from 'vue';
import Toast from '@/components/toast';

const app = createApp(App);
app.use(Toast);
app.mount('#app');

// 定义了全局方法之后需要扩充类型
declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $toast: typeof Toast;
  }
}
复制代码

使用

代码语言:javascript复制
this.$toast('message');

this.$toast({
  message: 'message',
  dutation: 5000,
})
复制代码

和 vue2 上的对比

代码语言:javascript复制
vue2vue3创建方式1. 先创建构造函数  const MessageBox = Vue.extend(MessageComp);  2. 通过构造函数实例化对象 const instance: any  = new MessageBox({ data: options, }).$mount();1. 先创建一个 div 容器 const container = document.createElement('div');   2. createVNode 创建组件实例 const vm = createVNode(Message, opts);  3. 把实例 render 到容器上 render(vm, container);

0 人点赞