I. Component introduction

El-message is an information prompt component, which is often used for system-level reminders. In my project, the El-message component is used to display user operation prompts and operation results, such as: operation success, operation failure: XXXX cause.

The use of el-Message is different from normal Element-Plus components in that it is not introduced by using tags in the template, but is called in JS code through the API. Example:

ElMessage.success( 'Congratulations, this is a success.');
Copy the code

Second, source code analysis

The source code for El-Message consists of two parts: component presentation and API methods

2.1 Packages \ Components \message\ SRC \index.vue

This section uses the VUE file and is mainly used to display the message content

<template>
  <! -- Transition component, bind before-leave after-leave hook -->
  <transition name="el-message-fade" @before-leave="onClose" @after-leave="$emit('destroy')">
    <! -- Bind the mouseEnter mouseleave event to clear/start the timer -->
    <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"
    >
      <! -- icon, displayed by type, bound to incoming iconClass -->
      <i v-if="type || iconClass" :class="['el-message__icon', typeClass, iconClass]"></i>
      <! -- Default slot -->
      <slot>
        <! -- Message is displayed as text when dangerouslyUseHTMLString is not enabled -->
        <p v-if=! "" dangerouslyUseHTMLString" class="el-message__content">{{ message }}</p>
        <! -- Open dangerouslyUseHTMLString to display as Html content -->
        <! Note that V-HTML can cause XSS attacks. Do not use untrusted user input as message.
        <! -- eslint-disable-next-line -->
        <p v-else class="el-message__content" v-html="message"></p>
      </slot>
      <! -- Show the close icon -->
      <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: {
    // Customize the class name
    customClass: { type: String.default: ' ' },
    // Whether to center the display
    center: { type: Boolean.default: false },
    // Whether to enable v-HTML
    dangerouslyUseHTMLString: { type: Boolean.default: false },
    // Message displays the time, default 3s
    duration: { type: Number.default: 3000 },
    // The icon class will override the default type class
    iconClass: { type: String.default: ' ' },
    // id
    id: { type: String.default: ' ' },
    // Message content, which can be string or vNode
    message: {
      type: [String.Object] as PropType<string | MessageVM>,
      default: ' ',},// Close the callback function
    onClose: {
      type: Function as PropType<() = > void>,
      required: true,},// Whether to display the close icon
    showClose: { type: Boolean.default: false },
    // Message type, SUCCESS warning info error
    type: { type: String.default: 'info' },
    // Offset from the top
    offset: { type: Number.default: 20 },
    / / zIndex values
    zIndex: { type: Number.default: 0}},// Outgoing events
  emits: ['destroy'].setup(props) {
    // The typeClass used by the icon
    const typeClass = computed(() = > {
      // if props. IconClass has a value, return ''
      consttype = ! props.iconClass && props.typereturn type && TypeMap[type]
        ? `el-icon-${TypeMap[type]}`
        : ' '
    })
    // Dynamic style, set top and zindex
    const customStyle = computed(() = > {
      return {
        top: `${props.offset}px`.zIndex: props.zIndex,
      }
    })

    const visible = ref(false)
    let timer = null
    // Start the timer, duration default is 3s, to the time automatically hidden
    function startTimer() {
      if (props.duration > 0) {
        timer = setTimeout(() = > {
          if (visible.value) {
            close()
          }
        }, props.duration)
      }
    }
    // Clear timer, called when mouseEnter
    function clearTimer() {
      clearTimeout(timer)
      timer = null
    }

    function close() {
      visible.value = false
    }
    // Listen for keyboard keystroke events,
    function keydown({ code }: KeyboardEvent) {
      if (code === EVENT_CODE.esc) {
        // Press ESC to close the display
        if (visible.value) {
          close()
        }
      } else {
        // Other buttons to restart the timer
        startTimer() // resume timer}}/ / mount
    onMounted(() = > {
      // Start timer
      startTimer()
      // Make the content visible
      visible.value = true
      // Listen for keyDown events on document
      on(document.'keydown', keydown)
    })
    / / unloading
    onBeforeUnmount(() = > {
      // Cancel keyDown listening
      off(document.'keydown', keydown)
    })

    return {
      typeClass,
      customStyle,
      visible,

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

Copy the code

2.2 Api parts Packages \ Components \message\ SRC \message.ts

This part of the function is to provide Api for external use

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";

// A global variable that stores all message instances and makes it easy to change the vertical offset of message instances
const instances: MessageQueue = [];
// increment count, new instance seed++
let seed = 1;

const Message: IMessage = function (opts: MessageParams = {} as MessageParams) :IMessageHandle {
  if (isServer) return;
  // The argument passed in can be of type string
  // String is converted to an object
  if (typeof opts === "string") {
    opts = {
      message: opts,
    };
  }

  let options: IMessageOptions = <IMessageOptions>opts;

  // Vertical offset from the top, default is 20px
  let verticalOffset = opts.offset || 20;
  // The new Message popup is displayed under the old Message popup
  // The vertical offset is added to the distance of the existing Message popup
  instances.forEach(({ vm }) = > {
    verticalOffset += (vm.el.offsetHeight || 0) + 16;
  });
  verticalOffset += 16;
  // Incrementing the unique ID
  const id = "message_" + seed++;
  // The callback function passed by the user to close
  const userOnClose = options.onClose;

  // Assemble the new options as props for the Message componentoptions = { ... options,onClose: () = > {
      close(id, userOnClose);
    },
    offset: verticalOffset,
    id,
    // Get the latest zIndex
    zIndex: PopupManager.nextZIndex(),
  };

  // Create a new div element as a container for the popbox
  const container = document.createElement("div");
  container.className = `container_${id}`;

  const message = options.message;
  // MessageConstructor (message/ SRC /index.vue) is the constructor of the message component
  // options are props for the message component
  // If options.message is passed a vnode, render that vnode as the children of the component
  const vm = createVNode(MessageConstructor, options, isVNode(options.message) ? { default: () = > message } : null);

  // The effect is the same as @destroy on the component. The trainsition after-leave hook will emit destroy and execute this method
  // Remove the message component to avoid memory leaks
  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.
  };
  // Render the message component into the container
  render(vm, container);
  // Put the Message component into the Instances
  // In the close function, the VM is removed from the instance
  instances.push({ vm });
  // Mount the container to the body
  document.body.appendChild(container.firstElementChild);

  return {
    // There is a close method in the returned object, which the user can use to close the display
    // This close method sets visible to false, which raises the Transition before-leave/after-leave hook function
    close: () = > ((vm.component.proxy as ComponentPublicInstance<{ visible: boolean }>).visible = false),}; }as any;

// The transition before-leave hook calls onClose
// The onClose method saves the ID through a closure and calls the close method to close the Message and destroy the component
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;
  // Call the user-passed onClose hook functionuserOnClose? .(vm);const removedHeight = vm.el.offsetHeight;
  // Remove the current VM from instances
  instances.splice(idx, 1);

  // Adjust the height of other Message instances
  const len = instances.length;
  if (len < 1) return;
  // Just adjust the height of the instance whose index is greater than the current 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; }}// Close all messages
export function closeAll() :void {
  // instances call their close methods one by one
  for (let i = instances.length - 1; i >= 0; i--) {
    const instance = instances[i].vm.component as any; instance.ctx.close(); }}// Add corresponding methods for each of the four types
(["success"."warning"."info"."error"] as const).forEach((type) = > {
  Message[type] = (options) = > {
    if (typeof options === "string") {
      options = {
        message: options,
        type}; }else {
      options.type = type;
    }
    // Call the Message method to generate Message
    return Message(options);
  };
});

Message.closeAll = closeAll;
export default Message;
Copy the code

2.3 summarize

  1. Use the Instances variable to maintain all Message instances, which is convenient to control the top offset of each instance.
  2. The Message instance is destroyed in the transition before-leave/after-leave hook function