简介

音讯提醒组件 Message 罕用于被动操作后的反馈提醒,顶部居中显示并主动隐没,是一种不打断用户操作的轻量级提醒形式。本文将剖析其源码实现,急躁读完,置信会对您有所帮忙。 组件文档 Message gitee源码

更多组件分析详见 Element 2 源码分析组件总览

应用形式

组件Message以服务的形式调用。Message 组件入口文件中没有,没有插件申明,只是导出了办法 Message;在组件库入口文件中,将办法 Message增加至 Vue.prototype

// `Message` 组件入口文件// packages\message\index.jsimport Message from './src/main.js';export default Message; 

在组件库入口文件也是一样的解决。

// 组件库入口 // src/index.jsimport Message from '../packages/message/index.js';//... const install = function(Vue, opts = {}) {  //...  Vue.prototype.$message = Message; }; export default {  //...  Message,}; 

调用办法为 Message(options)。组件也为每个 type 定义了各自的办法,如 Message.success(options)。调用 Message.closeAll() 手动敞开所有实例。组件库残缺引入,间接应用this.$message(options)

// 残缺引入this.$message(options);// 独自援用import { Message } from 'element-ui';// ...Message(options);

其中 options 参数为 Message 的配置项,在此不做详尽解释,详见 组件文档 Message #options。

组件源码

HTML

音讯提醒组件页面元素构造比较简单。

根节点下元素依照功辨别,次要有三局部:

  • Icon 图标
  • 音讯文字
  • 敞开按钮

应用 transition 组件,在组件根节点的条件展现 (v-show)中增加过渡成果,定义了钩子函数after-leave 用于设置过渡来到实现之后的组件状态。

// packages\message\src\main.vue<template>  <!-- transition过渡组件,绑定after-leave钩子 -->  <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" >      <!-- 主题/自定义图标 -->      <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>    // 主题类型图标映射  const typeMap = {    success: 'success',    info: 'info',    warning: 'warning',    error: 'error'  };    export default {    data() {      return {        visible: false, // 组件显示状态        message: '', // 音讯文字        duration: 3000, // 显示工夫, 毫秒        type: 'info', // 状态主题        iconClass: '', // 自定义图标的类名        customClass: '', // 自定义类名        onClose: null, // 敞开时的回调函数        showClose: false, // 是否显示敞开按钮        closed: false, // 组件敞开状态        verticalOffset: 20, // 间隔顶部的偏移 top: 20px        timer: null, // 定时器,管制组件主动敞开        dangerouslyUseHTMLString: false, // 是否将 message 属性作为 HTML 片段解决        center: false // 文字是否居中      };    },      computed: {      // 不同主题type的图标      typeClass() {        return this.type && !this.iconClass          ? `el-message__icon el-icon-${ typeMap[this.type] }`          : '';      },      // 设置top      positionStyle() {        return {          'top': `${ this.verticalOffset }px`        };      }    },    // ...  };</script> 

top 偏移量

元素根节点是一个类名el-message的div元素,应用相对布局。fixed示意脱离文档流,通过指定元素绝对于屏幕视口(viewport)的地位来指定元素地位,程度方向居中,垂直方向居上。

.el-message {   position: fixed;  left: 50%;  top: 20px; }

应用计算属性 positionStyle 基于设置的verticalOffset属性值动静计算组件间隔顶部的偏移量。

// top: 20pxpositionStyle() {  return {    'top': `${ this.verticalOffset }px`  };}

页面中能够存在多个Message 实例,新 Message 音讯会在旧的上面展现,此时实例依据在数组中所处索引值,计算出实例的间隔顶部的偏移量 verticalOffset。组件实例随着主动/人工敞开销毁,数组内容变量,其索引值会变动,verticalOffset值也会从新计算,下文“服务实现”一节中会具体介绍该逻辑。

状态主题

状态主题属性type默认值 info, 组件反对success/warning/info/error共四种可选值。

根节点中基于type值生成不同主题款式el-message--[success/warning/info/error]。但当传入属性iconClass值用于自定义图标的类名,就不会生成主题款式,此时 type设置有效。

type && !iconClass ? `el-message--${ type }` : '',

子元素内容布局

message 组件外部应用flex布局。属性 center用于生成类名is-center设置图标和音讯文字居中。

.el-message {   display: flex;   align-items: center;} .el-message.is-center {   justify-content: center;}

敞开图标应用相对布局,垂直居中程度居右。

<!-- 敞开图标 --><i v-if="showClose" class="el-message__closeBtn el-icon-close" @click="close"></i>      .el-message__closeBtn {  position: absolute;  top: 50%;  right: 15px; }

过后显示敞开图标时,会生成类名is-closable ,避免音讯文字跟敞开按钮由重叠。

.el-message.is-closable .el-message__content {  padding-right: 16px;}

Icon图标优先显示自定义。主题图标的类名应用计算属性typeClass

<!-- 主题/自定义图标 --><i :class="iconClass" v-if="iconClass"></i><i :class="typeClass" v-else></i>

音讯文字插槽

属性 message 反对传入 HTML 片段,然而须要显示关上此性能(将属性dangerouslyUseHTMLString 设置 true)。

<!-- 默认插槽 --><slot>  <p v-if="!dangerouslyUseHTMLString" class="el-message__content">{{ message }}</p>  <p v-else v-html="message" class="el-message__content"></p></slot> 

当属性 message 传入值类型为VNode时,会应用插槽性能,下文“服务实现”一节会具体介绍。

// packages\message\src\main.jsif (isVNode(instance.message)) {    instance.$slots.default = [instance.message];    instance.message = null;  }
在网站上动静渲染任意 HTML 是十分危险的,因为容易导致 XSS 攻打。因而在 dangerouslyUseHTMLString 关上的状况下,请确保 message 的内容是可信的,永远不要将用户提交的内容赋值给 message 属性。

生命周期 & 事件

组件被挂载之后调用办法startTimer启用定时器,实现 message 实例的主动敞开。在办法startTimer中当属性duration值大于0时(if (this.duration > 0)),才会创立定时器用于主动敞开;若组件不须要主动敞开,将属性duration值设置为 0 即可。

挂载之后增加keydown事件监听。实例销毁之前,会移除keydown事件监听。办法 keydown用于实现按ESC键敞开音讯组件。如果页面存在多个实例,会全副敞开。

应用自定义侦听器,当属性closed值变动且为 true 时,将属性visible值置为false(组件暗藏)。

export default {  // ...   // 挂载时  mounted() {    // 启动定时器    this.startTimer();    // 监听keydown事件    document.addEventListener('keydown', this.keydown);  },  beforeDestroy() {    // 勾销keydown监听    document.removeEventListener('keydown', this.keydown);  },  watch: {    closed(newVal) {      if (newVal) {        this.visible = false;      }    }  },   methods: {     // 组件敞开事件    close() {      // ...    },    // 过渡`after-leave`钩子函数    handleAfterLeave() {      // ...    },    // 革除定时器,当mouseenter时调用    clearTimer() {      clearTimeout(this.timer);    },    // 启动定时器,duration默认是3s,到工夫自动隐藏    startTimer() {      if (this.duration > 0) {        this.timer = setTimeout(() => {          if (!this.closed) {            this.close();          }        }, this.duration);      }    },    // 监听键盘按键事件    keydown(e) {      if (e.keyCode === 27) { // esc敞开音讯        if (!this.closed) {          this.close();        }      }    }  },};

根节点绑定 mouseentermouseleave 事件,当鼠标挪动到 message 实例上,会革除其定时器clearTimer,该实例就不会主动敞开。当鼠标移出后,会从新创立定时器startTimer,实现主动敞开。

<div @mouseenter="clearTimer"  @mouseleave="startTimer" >  // ...</div>

办法close用于敞开组件,如果用户设置了属性onClose值,敞开时会执行该回调函数, 参数为被敞开的 message 实例。

close() {  this.closed = true;  if (typeof this.onClose === 'function') {    this.onClose(this);  }},

当组件敞开后,会触发transition 组件绑定after-leave钩子函数,执行办法 handleAfterLeave。过渡来到实现之后,执行办法vm.$destroy(),齐全销毁该实例,触发 beforeDestroy 的钩子;同时将实例 DOM 元素从页面移除

handleAfterLeave() {  // 齐全销毁一个实例。清理它与其它实例的连贯,解绑它的全副指令及事件监听器  // 触发 beforeDestroy 和 destroyed 的钩子  this.$destroy(true);  this.$el.parentNode.removeChild(this.$el);},

服务形式源码

组件服务形式实现的源码文件为 packages\message\src\main.js

源码精简后构造如下,代码创立了function类型的对象Message,同时给对象增加属性办法 closecloseAllwarninginfoerror,导出对象 Message

// packages\message\src\main.js let MessageConstructor = Vue.extend(Main);  // message组件结构器let instance; // message 组件实例 let instances = []; // 存储所有message实例数组 let seed = 1; // 用于递增计数const Message = function(options) {  // 逻辑 ...  return instance;};  // 定义了各状态的便捷办法 Message.success(options)['success', 'warning', 'info', 'error'].forEach(type => {  // 逻辑 ...});// 敞开指定id 的Message实例Message.close = function(id, userOnClose) {  // 逻辑 ...};// 敞开所有Message实例Message.closeAll = function() {  // 逻辑 ...}; export default Message; 

Message()

应用函数表达式(函数字面量)形式,将函数赋值给了变量 Message。 该函数用于初始化配置创立组件,并返回该实例。

函数实现性能次要有以下步骤:

  1. 参数 options 初始化。
  2. 应用Vue.extendvm.$mount()创立渲染挂载实例,默认将其增加至body元素节点下。
  3. 依据数组instances中实例数量,计算并设置该实例顶部的垂直偏移。
  4. 将实例设置显示可见 visible = true
  5. 更新数组instances,将该实例增加至其中。
  6. 返回message 实例。

每个实例生成惟一ID,格局为message_xx,用于实例敞开操作,稍后会详尽解释。

// 此处代码未作详尽解释let MessageConstructor = Vue.extend(Main);  // message组件结构器let instance; // message 组件实例 let instances = []; // 存储所有message实例数组 let seed = 1; // 用于递增计数const Message = function(options) {     // options 初始化...    // 实例创立渲染  let id = 'message_' + seed++; // 组件实例 id  // 创立组件实例  instance = new MessageConstructor({    data: options  });  instance.id = id;    instance.$mount(); // 渲染为文档之外的的元素   document.body.appendChild(instance.$el); // 挂载实例 增加至body元素节点下    // 计算并设置顶部的垂直偏移 ...    instance.visible = true; // 组件显示可见  instance.$el.style.zIndex = PopupManager.nextZIndex(); // 实例元素zIndex  全局对立治理  instances.push(instance); // 增加至数组中    return instance;};

options 类型格式化

options参数传入不是string类型时,例如 this.$message('音讯文字');,定义对象并传入的参数值赋值给属性 message,等同于 this.$message({ message:'音讯文字'});

if (typeof options === 'string') {  options = {    message: options  };}

VNode反对

当属性message值传入一个 VNode 时,将其赋值给匿名插槽,此时插槽的后备内容不会被渲染。

// 逻辑实现// packages\message\src\main.js if (isVNode(instance.message)) {  instance.$slots.default = [instance.message];  instance.message = null;}// 调用形式const h = this.$createElement;this.$message({  message: h("p", null, [    h("span", null, "内容能够是 "),    h("i", { style: "color: teal" }, "VNode"),  ]),});

办法isVNode应用“鸭式辨型法”判断参数值是否为VNode类型。
VNode类型更多内容请查看 VNode class declaration 。

export function isVNode(node) {  return node !== null && typeof node === 'object' && hasOwn(node, 'componentOptions');};

间隔窗口顶部偏移量计算

页面中能够存在多个Message 实例,新 Message 音讯会在旧的上面展现,也就是依照创立工夫由早到晚,实例从上到下顺次展现。

  • 首个显示(最下面)的实例的偏移量由属性offset值管制。
  • 16用于设置多个实例显示时,实例元素之间的间距。
  • offsetHeight 返回实例元素的像素高度,高度蕴含该元素的垂直内边距和边框,且是一个整数。
  • 新创建的实例在计算完后偏移量后才会将最增加至数组中。
// 计算间隔窗口顶部偏移量计算 let verticalOffset = options.offset || 20; // 默认是20px// 新的Message弹框在旧的Message弹框上面展现  垂直偏移要加上以后已有的Message弹框的间隔instances.forEach(item => {  verticalOffset += item.$el.offsetHeight + 16; });instance.verticalOffset = verticalOffset; // 更新偏移量// ...// 增加至数组中instances.push(instance); 

新创建的实例间隔窗口顶部偏移量verticalOffset计算公式如下:

verticalOffset = offset/20 + ( 实例元素高度(offsetHeight) + 16 ) *显示实例个数(instances.length)

数组instances 用于寄存页面可见(未敞开销毁)的实例。当实例敞开后,数组更新操作会在随后具体解说。

Message.close()

属性办法 close由两个参数:组件id(创立实例时生成的,格局为message_xx)、用户传入的敞开时回调函数,用于管制整个页面实例数组以及偏移量计算,执行用户传入的敞开时回调函数。

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); // 从数组instances中去掉移除该实例      break;    }  }  // 未找到该实例 或者 该实例之后没有元素 退出代码  if (len <= 1 || index === -1 || index > instances.length - 1) return;  // 只须要调整index 大于以后Message的实例偏移量  for (let i = index; i < len - 1 ; i++) {    let dom = instances[i].$el;    dom.style['top'] =      parseInt(dom.style['top'], 10) - removedHeight - 16 + 'px';  }};

办法实现性能次要有以下步骤:

  1. 依据组件id从数组中查找实例的索引index。
  2. 未找到对应实例,匹配条件 index === -1 ,退出办法。
  3. 若找到对应实例,记录实例索引index。

    • 获取实例元素 offsetHeight。
    • 调用用户传入的敞开时回调函数。
    • 从数组instances中去掉移除该实例。
    • 判断该索引后是否还有其余元素,没有的话,退出办法;有的话执行下一步。
    • 只调整 index 大于以后Message的实例的高度,也就是实例之后的实例元素。
    • 依据移除实例元素 offsetHeight 和 间距16,从新计算偏移量。

下图展示了敞开页面第二个实例后,随后的二个实例的偏移量须要从新计算。

组件的敞开流程

当初将各性能点串起来,解释下组件敞开时,产生了什么?

对象Message定义中,传入组件的敞开回调函数,不是用户传入的原始值,是做了一层包装。通过闭包将id和onClose回调函数作为参数,调用 Message.close() 办法。

即便用户没有传入敞开时回调函数,组件实例创立时也会有办法传入,用于组件敞开销毁后更新整个页面实例数组更新残余实例偏移量。

// packages\message\src\main.jsconst Message = function(options) {   // ...  let userOnClose = options.onClose; // 用户传入的敞开时的回调函数   let id = 'message_' + seed++; // 组件实例 id  // 敞开时 回调函数执行逻辑 Message.close  options.onClose = function() {    Message.close(id, userOnClose);  };   // ...};

当实例由敞开图标点击、定时器、ESC按键等形式触发敞开close办法时,必然会执行回调函数,相当于Message.close(id, userOnClose)
此时组件也会调用办法 handleAfterLeave 销毁实例移除DOM元素。

// packages\message\src\main.vue// template<transition name="el-message-fade" @after-leave="handleAfterLeave">   <div v-show="visible">    // ...  </div></transition> // methods handleAfterLeave() {   this.$destroy(true);  this.$el.parentNode.removeChild(this.$el);},close() {  this.closed = true;  if (typeof this.onClose === 'function') {    this.onClose(this);  }},
调用 Message 或 this.$message 会返回以后 Message 的实例。如果须要手动敞开实例,能够调用它的 close 办法。

Message.closeAll()

属性办法 closeAll用于敞开所有 message 实例。

遍历 instances,一一调用实例的close()办法。相当于按ESC键敞开成果。

Message.closeAll = function() {  for (let i = instances.length - 1; i >= 0; i--) {    instances[i].close();  }};
此处 close() 办法时组件外部定义的,不是 Message.close()

快捷办法

定义了各状态的便捷属性办法,例如 Message.success(options)。通过格式化参数,指定了options.type属性值。

// 定义了各状态的便捷办法 Message.success(options)['success', 'warning', 'info', 'error'].forEach(type => {  Message[type] = options => {    if (typeof options === 'string') {      options = {        message: options      };    }    options.type = type;    return Message(options);  };});

款式实现

组件款式源码 packages\theme-chalk\src\message.scss 应用混合指令 bwhenme 嵌套生成组件款式。

// 生成 .el-message@include b(message) {  // ...    // 生成 .el-message.is-center  @include when(center) {    // ...  }  @include when(closable) {    // 生成 .el-message.is-closable .el-message__content    .el-message__content {      // ...    }  }  // 生成 .el-message p  p {    // ...  }    @include m(info) {    // 生成 .el-message--info .el-message__content    .el-message__content {      // ...    }  }  // 生成 .el-message--success/warning/error  @include m(success) {    // ...        // 生成 .el-message--success/warning/error .el-message__content    .el-message__content {      // ...    }  }   // warning/error 省略...      // 生成 .el-message__icon  @include e(icon) {    // ...  }  // 生成 .el-message__content  @include e(content) {    // ...        // 生成 .el-message__content:focus    &:focus {      // ...    }  }  // 生成 .el-message__closeBtn  @include e(closeBtn) {    // ...        // 生成 .el-message__closeBtn:focus    &:focus {      // ...    }    // 生成 .el-message__closeBtn:hover    &:hover {      // ...    }  }  // 生成  .el-icon-success/error/info/warning  & .el-icon-success {    // ...  }  // error/info/warning 省略...}// 生成 .el-message-fade-enter,.el-message-fade-leave-active.el-message-fade-enter,.el-message-fade-leave-active {  // ...}

参考&关联浏览

'api/Vue-extend',vuejs
'transitions#JavaScript 钩子',vuejs
'CSS/position',MDN
自定义指令,vuejs
'HTMLElement/offsetHeight',MDN

关注专栏

如果本文对您有所帮忙请关注➕、 点赞、 珍藏⭐!您的认可就是对我的最大反对!