Element 2 组件源码剖析之Message消息提示(上)-组件实现

1,646 阅读2分钟

简介

消息提示组件 Message 常用于主动操作后的反馈提示,顶部居中显示并自动消失,是一种不打断用户操作的轻量级提示方式。本文将分析其源码实现,耐心读完,相信会对您有所帮助。🔗 组件文档 Message 🔗 gitee源码

更多组件分析详见 👉 📚 Element UI 源码剖析组件总览

本专栏的 gitbook 版本地址已经发布 📚《learning element-ui》 ,内容同步更新中!

使用方式

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

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

在组件库入口文件也是一样的处理。

// 组件库入口 
// src/index.js
import 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: 20px
positionStyle() {
  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.js
if (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);
},

📚参考&关联阅读

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

关注专栏

此文章已收录到专栏中 👇,可以直接关注。