从Element2开始--message

1,468 阅读3分钟

前言

  element-业界驰名

  自己准备通过这一系列文章,将element2-message、element Plus等ui框架源码的分析分享出来,把Vue2、Vue3基础、TypeScript、jest、monorepo项目管理模式等知识点串联一遍,熟悉一下。

结构分析

  github上clone项目之后,packages/message便是咱们的目标目录。

image.png   目录很简单,index.js引入main.js之后,暴露出来,然后在人口文件中引入,然后以别名$message形式挂载在Vue.prototype上。

image.png

main.vue

<template>
  <transition name="el-message-fade" @after-leave="handleAfterLeave">
    <div
      :class="[
        'el-message',
        type && !iconClass ? `el-message--${type}` : '',
        center ? 'is-center' : '',
        showClose ? 'is-closable' : '',
        customClass,
      ]"
      :style="positionStyle"
      v-show="visible"
      @mouseenter="clearTimer"
      @mouseleave="startTimer"
      role="alert"
    >
      <i :class="iconClass" v-if="iconClass"></i>
      <i :class="typeClass" v-else></i>
      <slot>
        <p v-if="!dangerouslyUseHTMLString" class="el-message__content">
          {{ message }}
        </p>
        <p v-else v-html="message" class="el-message__content"></p>
      </slot>
      <i
        v-if="showClose"
        class="el-message__closeBtn el-icon-close"
        @click="close"
      ></i>
    </div>
  </transition>
</template>

<script type="text/babel">
const typeMap = {
  success: "success",
  info: "info",
  warning: "warning",
  error: "error",
};

export default {
  data() {
    return {
      visible: false,
      // 消息文字
      message: "",
      // 显示时间, 毫秒。设为 0 则不会自动关闭
      duration: 3000,
      // 类型:success/warning/info/error
      type: "info",
      // 自定义class-icon类名
      iconClass: "",
      // 自定义类名
      customClass: "",
      // 关闭时的回调函数, 参数为被关闭的 message 实例
      onClose: null,
      // 是否显示关闭按钮
      showClose: false,
      // 本组件用于判断结束显示的标志
      closed: false,
      // 高度,用于多个实例同时出现时,往下加一定的高度
      verticalOffset: 20,
      // 定时器,三秒关闭这个实例
      timer: null,
      // 是否将 message 属性作为 HTML 片段处理
      dangerouslyUseHTMLString: false,
      // 居中显示
      center: false,
    };
  },

  computed: {
    // 根据类型显示不同的风格类名
    typeClass() {
      return this.type && !this.iconClass
        ? `el-message__icon el-icon-${typeMap[this.type]}`
        : "";
    },
    // 距离顶部的距离
    positionStyle() {
      return {
        top: `${this.verticalOffset}px`,
      };
    },
  },

  watch: {
    // 为什么要多立个flag?
    closed(newVal) {
      if (newVal) {
        this.visible = false;
      }
    },
  },

  methods: {
    // 动画执行完销毁el
    handleAfterLeave() {
      this.$destroy(true);
      this.$el.parentNode.removeChild(this.$el);
    },
    // 关闭操作,如果有回调,执行回调
    close() {
      this.closed = true;
      if (typeof this.onClose === "function") {
        this.onClose(this);
      }
    },
    // 清除定时器,鼠标放上去的时候,并不会消失
    clearTimer() {
      clearTimeout(this.timer);
    },
    // 实例一开始就开始执行定时器,鼠标一进来又重置
    startTimer() {
      if (this.duration > 0) {
        this.timer = setTimeout(() => {
          if (!this.closed) {
            this.close();
          }
        }, this.duration);
      }
    },
    // 监听ESC键
    keydown(e) {
      if (e.keyCode === 27) {
        // esc关闭消息
        if (!this.closed) {
          this.close();
        }
      }
    },
  },
  mounted() {
    this.startTimer();
    document.addEventListener("keydown", this.keydown);
  },
  beforeDestroy() {
    document.removeEventListener("keydown", this.keydown);
  },
};
</script>

  上面代码是main.vue的源码,清晰明了。之前没有在意过一个小功能就是鼠标放上去,实例会一直显示,代码中通过@mouseenter="clearTimer"来实现的。还有this.$destroy(true),这个参数true在Vue2不用加,感谢倔友的分享。

decode.png   顺便分享下Vue2中关于$destroy的源码

destroy.png   可以看出$destory中没有接收参数,并在函数中依次调用了beforeDestroy、destroyed方法。

main.js

  相比main.vue,main.js文件才是重头戏。

import Vue from 'vue';
import Main from './main.vue';
import { PopupManager } from 'element-ui/src/utils/popup';
import { isVNode } from 'element-ui/src/utils/vdom';
let MessageConstructor = Vue.extend(Main);

// MessageConstructor实例
let instance;
let instances = [];
// 用来给instances数组中每个实例设置id
let seed = 1;

const Message = function(options) {
  // 服务端不得用哦!
  if (Vue.prototype.$isServer) return;
  // 调用this.$message时传递进来的参数
  options = options || {};
  // 如果是this.$message("测试弹出")这样的,可以直接转换
  if (typeof options === 'string') {
    options = {
      message: options
    };
  }
  // 关闭回调
  let userOnClose = options.onClose;
  // 实例标识
  let id = 'message_' + seed++;
  // 添加关闭回调
  options.onClose = function() {
    Message.close(id, userOnClose);
  };
  // 创建message实例
  instance = new MessageConstructor({
    data: options
  });
  instance.id = id;
  // 接收render函数
  if (isVNode(instance.message)) {
    // https://cn.vuejs.org/v2/guide/render-function.html?  
    // 默认插槽接收一个节点数组
    instance.$slots.default = [instance.message];
    instance.message = null;
  }
  // 手动挂载
  instance.$mount();
  document.body.appendChild(instance.$el);
  // 多个实例的时候,逐个下沉16px
  let verticalOffset = options.offset || 20;
  instances.forEach(item => {
    verticalOffset += item.$el.offsetHeight + 16;
  });
  instance.verticalOffset = verticalOffset;
  // 展示实例
  instance.visible = true;
  // 处理z-index
  instance.$el.style.zIndex = PopupManager.nextZIndex();
  // 放入实例数组中
  instances.push(instance);
  return instance;
};
// 取别名 
['success', 'warning', 'info', 'error'].forEach(type => {
  Message[type] = options => {
    if (typeof options === 'string') {
      options = {
        message: options
      };
    }
    options.type = type;
    return Message(options);
  };
});
// 关闭删除实例
Message.close = function(id, userOnClose) {
  // 实例数量
  let len = instances.length;
  let index = -1;
  // 找到要关闭的实例下标
  let removedHeight;
  for (let i = 0; i < len; i++) {
    if (id === instances[i].id) {
      removedHeight = instances[i].$el.offsetHeight;
      index = i;
      // 如果有关闭回调
      if (typeof userOnClose === 'function') {
        userOnClose(instances[i]);
      }
      instances.splice(i, 1);
      break;
    }
  }
  if (len <= 1 || index === -1 || index > instances.length - 1) return;
  // 从删除的下标开始往回缩减高度
  for (let i = index; i < len - 1 ; i++) {
    let dom = instances[i].$el;
    dom.style['top'] =
      parseInt(dom.style['top'], 10) - removedHeight - 16 + 'px';
  }
};

Message.closeAll = function() {
  for (let i = instances.length - 1; i >= 0; i--) {
    instances[i].close();
  }
};

export default Message;

  关于main.js的逻辑是:定义Message函数-->取别名-->添加close(删除某个实例的方法)/closeAll方法;Message函数的逻辑是:判断赋值options-->添加关闭回调-->创建MessageConstructor实例-->赋值实例id-->手动挂载-->ui响应-->展示实例-->放入实例数组

  开头引用的PopupManager是用来设置z-index的,具体可以参考这篇文章 关于使用element中的popup问题 ;引入的VNode方法是为了判断节点是否是render函数;

image.png

   关于Vue.extend在平时做Vue项目时用的不多,所以这次正好复习了这个知识点。Vue.extend()

extend.png   从Vue.extend的源码中可以知道,在合并extendOptions的时候,并没有解除其映射,所以经过如下操作之后,options同该实例中的data映射的是同一个地址。通过打印options就可以看出来。

image.png    关于手动挂载:Vue.$mount() ;使用 vm.$mount() 手动地挂载一个未挂载的实例,如果没有提供 elementOrSelector 参数,模板将被渲染为文档之外的的元素,并且你必须使用原生 DOM API 把它插入文档中。

  阅读源码是一种很好的学习途径,写这篇文章也是为了记录分享一下自己的心得体会,希望各位大佬多多指教。相关的源码,已经上传至github,请各位自行查阅。element-source-code