[Element Plus 源码解析] Message 消息提示组件

一、组件介绍

el-message是一个信息提示类组件,常用于系统级别的提醒,在我的项目中,会使用el-message组件展示用户的操作提示及操作结果,如:操作成功、操作失败:xxxx原因

image.png

el-message的使用与一般的element-plus组件有所不同,其不是通过在template中使用标签的方式引入,而是在js代码中,通过api的方式进行调用。
使用示例:

ElMessage.success( '恭喜你,这是一条成功消息');
复制代码

二、源码分析

el-message的源码有2部分:组件展示部分和api方法

2.1 组件展示部分 packages\components\message\src\index.vue

该部分使用vue文件,主要作用是展示消息内容

<template>
  <!-- 过渡组件,绑定before-leave after-leave钩子 -->
  <transition name="el-message-fade" @before-leave="onClose" @after-leave="$emit('destroy')">
    <!-- 绑定mouseenter mouseleave事件,清除/启动定时器 -->
    <div
      v-show="visible"
      :id="id"
      :class="[
        'el-message',
        type && !iconClass ? `el-message--${type}` : '',
        center ? 'is-center' : '',
        showClose ? 'is-closable' : '',
        customClass,
      ]"
      :style="customStyle"
      role="alert"
      @mouseenter="clearTimer"
      @mouseleave="startTimer"
    >
      <!-- 图标,根据类型展示,绑定传入的iconClass -->
      <i v-if="type || iconClass" :class="['el-message__icon', typeClass, iconClass]"></i>
      <!-- 默认插槽 -->
      <slot>
        <!-- 未开启dangerouslyUseHTMLString时,message作为文本内容展示 -->
        <p v-if="!dangerouslyUseHTMLString" class="el-message__content">{{ message }}</p>
        <!-- 开启dangerouslyUseHTMLString时,作为Html内容展示-->
        <!-- 需要注意的是,v-html可能引起xss攻击,不要使用不信任的用户输入作为message  -->
        <!--  eslint-disable-next-line -->
        <p v-else class="el-message__content" v-html="message"></p>
      </slot>
      <!-- 展示关闭图标 -->
      <div v-if="showClose" class="el-message__closeBtn el-icon-close" @click.stop="close"></div>
    </div>
  </transition>
</template>
<script lang="ts">
import { defineComponent, computed, ref, onMounted, onBeforeUnmount } from 'vue'
import { EVENT_CODE } from '@element-plus/utils/aria'
import { on, off } from '@element-plus/utils/dom'

// MessageVM is an alias of vue.VNode
import type { PropType } from 'vue'
import type { Indexable } from '@element-plus/utils/types'
import type { MessageVM } from './types'
const TypeMap: Indexable<string> = {
  success: 'success',
  info: 'info',
  warning: 'warning',
  error: 'error',
}
export default defineComponent({
  name: 'ElMessage',
  props: {
    // 自定义class name
    customClass: { type: String, default: '' },
    // 是否居中显示
    center: { type: Boolean, default: false },
    // 是否开启v-html功能
    dangerouslyUseHTMLString: { type: Boolean, default: false },
    // message显示的时间,默认3s
    duration: { type: Number, default: 3000 },
    // 提示图标的class,会覆盖默认的type class
    iconClass: { type: String, default: '' },
    // id
    id: { type: String, default: '' },
    // 消息内容,可以是string类型,或者是vnode
    message: {
      type: [String, Object] as PropType<string | MessageVM>,
      default: '',
    },
    // 关闭时的回调函数
    onClose: {
      type: Function as PropType<() => void>,
      required: true,
    },
    // 是否展示关闭图标
    showClose: { type: Boolean, default: false },
    // message的类型,success warning info error
    type: { type: String, default: 'info' },
    // 距离顶部的偏移
    offset: { type: Number, default: 20 },
    // zIndex值
    zIndex: { type: Number, default: 0 },
  },
  // 向外发射的事件
  emits: ['destroy'],
  setup(props) {
    // 图标使用的typeClass
    const typeClass = computed(() => {
      // 若props.iconClass有值,则返回''
      const type = !props.iconClass && props.type
      return type && TypeMap[type]
        ? `el-icon-${TypeMap[type]}`
        : ''
    })
    // 动态style,设置top和zindex
    const customStyle = computed(() => {
      return {
        top: `${props.offset}px`,
        zIndex: props.zIndex,
      }
    })

    const visible = ref(false)
    let timer = null
    // 启动定时器,duration默认是3s,到时间自动隐藏
    function startTimer() {
      if (props.duration > 0) {
        timer = setTimeout(() => {
          if (visible.value) {
            close()
          }
        }, props.duration)
      }
    }
    // 清除定时器,当mouseenter时调用
    function clearTimer() {
      clearTimeout(timer)
      timer = null
    }

    function close() {
      visible.value = false
    }
    // 监听键盘按键事件,
    function keydown({ code }: KeyboardEvent) {
      if (code === EVENT_CODE.esc) {
        // esc按键,则关闭显示
        if (visible.value) {
          close()
        }
      } else {
        // 其他按键,重启计时器
        startTimer() // resume timer
      }
    }

    // 挂载时
    onMounted(() => {
      // 启动定时器
      startTimer()
      // 设置内容可见
      visible.value = true
      // 在document上监听keydown事件
      on(document, 'keydown', keydown)
    })
    // 卸载时
    onBeforeUnmount(() => {
      // 取消keydown监听
      off(document, 'keydown', keydown)
    })

    return {
      typeClass,
      customStyle,
      visible,

      close,
      clearTimer,
      startTimer,
    }
  },
})
</script>

复制代码

2.2 Api部分 packages\components\message\src\message.ts

该部分作用是提供Api对外使用

import { createVNode, render } from "vue";
import { isVNode } from "@element-plus/utils/util";
import PopupManager from "@element-plus/utils/popup-manager";
import isServer from "@element-plus/utils/isServer";
import MessageConstructor from "./index.vue";

import type { ComponentPublicInstance } from "vue";
import type { IMessage, MessageQueue, IMessageOptions, MessageVM, IMessageHandle, MessageParams } from "./types";

// 全局变量,存储所有message实例,便于修改message实例的垂直offset
const instances: MessageQueue = [];
// 递增计数,新建实例时seed++
let seed = 1;

const Message: IMessage = function (opts: MessageParams = {} as MessageParams): IMessageHandle {
  if (isServer) return;
  // 传入的参数,可以是string类型
  // string类型时,转换成对象形式
  if (typeof opts === "string") {
    opts = {
      message: opts,
    };
  }

  let options: IMessageOptions = <IMessageOptions>opts;

  // 距离顶部的垂直偏移,默认是20px
  let verticalOffset = opts.offset || 20;
  // 新的Message弹框在旧的Message弹框下面展示
  // 垂直偏移要加上当前已有的Message弹框的距离
  instances.forEach(({ vm }) => {
    verticalOffset += (vm.el.offsetHeight || 0) + 16;
  });
  verticalOffset += 16;
  // 递增的唯一id
  const id = "message_" + seed++;
  // 用户传递的close时的回调函数
  const userOnClose = options.onClose;

  // 组装成新的options,作为message组件的props
  options = {
    ...options,
    onClose: () => {
      close(id, userOnClose);
    },
    offset: verticalOffset,
    id,
    // 获得最新的zIndex
    zIndex: PopupManager.nextZIndex(),
  };

  // 创建一个新的div元素,作为弹框的容器
  const container = document.createElement("div");
  container.className = `container_${id}`;

  const message = options.message;
  // MessageConstructor即message/src/index.vue,是message组件的构造器
  // options作为message组件的props
  // 如果options.message传入的是一个vnode,则将此vnode作为组件的children渲染
  const vm = createVNode(MessageConstructor, options, isVNode(options.message) ? { default: () => message } : null);

  // 效果等同于在组件上@destroy,trainsition组件的after-leave钩子会emit destroy,进而执行此方法
  // 删除message组件,避免内存泄漏
  vm.props.onDestroy = () => {
    render(null, container);
    // since the element is destroy, then the VNode should be collected by GC as well
    // we do not want cause any mem leak because we have returned vm as a reference to users
    // so that we manually set it to false.
  };
  // 将message组件,渲染到container中
  render(vm, container);
  // 将message组件放入instances中
  // 在close函数中,会将vm从instance中删除掉
  instances.push({ vm });
  // 将container挂载到body上
  document.body.appendChild(container.firstElementChild);

  return {
    // 返回对象中有close方法,使用者可以使用此close方法关闭显示
    // 该close方法将visible置为false,进而引发transition组件的before-leave/after-leave钩子函数
    close: () => ((vm.component.proxy as ComponentPublicInstance<{ visible: boolean }>).visible = false),
  };
} as any;

// transition的before-leave钩子会调用onClose方法
// onClose方法通过闭包将id保存下来,调用close方法关闭Message,销毁组件
export function close(id: string, userOnClose?: (vm: MessageVM) => void): void {
  const idx = instances.findIndex(({ vm }) => {
    const { id: _id } = vm.component.props;
    return id === _id;
  });
  if (idx === -1) {
    return;
  }

  const { vm } = instances[idx];
  if (!vm) return;
  // 调用用户传入的onClose钩子函数
  userOnClose?.(vm);

  const removedHeight = vm.el.offsetHeight;
  // 从instances中去掉当前vm
  instances.splice(idx, 1);

  // 调整其他Message实例的高度
  const len = instances.length;
  if (len < 1) return;
  // 只需要调整index 大于当前Message的实例的高度
  for (let i = idx; i < len; i++) {
    const pos = parseInt(instances[i].vm.el.style["top"], 10) - removedHeight - 16;

    instances[i].vm.component.props.offset = pos;
  }
}
// 关闭所有Message
export function closeAll(): void {
  // instances中逐个调用其close方法
  for (let i = instances.length - 1; i >= 0; i--) {
    const instance = instances[i].vm.component as any;
    instance.ctx.close();
  }
}

// 四种类型,都添加相应的方法
(["success", "warning", "info", "error"] as const).forEach((type) => {
  Message[type] = (options) => {
    if (typeof options === "string") {
      options = {
        message: options,
        type,
      };
    } else {
      options.type = type;
    }
    // 调用Message方法,生成Message
    return Message(options);
  };
});

Message.closeAll = closeAll;
export default Message;
复制代码

2.3 总结

  1. 使用instances变量维护所有的Message实例,方便控制各个实例的top offset;
  2. 在transition的before-leave/after-leave钩子函数中进行Message实例的销毁
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享