01-【第28期】源码分析:Element-UI的message 组件

225 阅读4分钟

寄语:参加若川组织的【源码共读】第28期——看Element-UI的message组件源码,期望收获更好的自己~~ 活动链接:www.yuque.com/ruochuan12/…

话不多说,正文开始!

首先说明一下:看的是element-ui给vue2使用的message组件。

一、使用用例

  1. element-ui提供的使用用例之一

默认的 Message 是不可以被人工关闭的,如果需要可手动关闭的 Message,可以使用showClose字段。此外,和 Notification 一样,Message 拥有可控的duration,设置0为不会被自动关闭,默认为 3000 毫秒。

<template>
  <!--下面2个按钮对应上图1:-->
  <el-button :plain="true" @click="open">打开消息提示</el-button>
  <el-button :plain="true" @click="openVn">VNode</el-button>
  <!--下面4个按钮对应上图2:-->
  <el-button :plain="true" @click="open1">消息</el-button>
  <el-button :plain="true" @click="open2">成功</el-button>
  <el-button :plain="true" @click="open3">警告</el-button>
  <el-button :plain="true" @click="open4">错误</el-button>
  <!--另一种写法:this.$message.error("这是一条错误信息!")-->
  <el-button :plain="true" @click="open5">错误</el-button>
</template>

<script>
  export default {
    methods: {
      // 传参:string类型
      open() {
        this.$message('这是一条消息提示');
      },
      
      // 传参:VNode类型
      openVn() {
        const h = this.$createElement;
        this.$message({
          message: h('p', null, [
            h('span', null, '内容可以是 '),
            h('i', { style: 'color: teal' }, 'VNode')
          ])
        });
      },
      
      // 下面4个传参:都是对象
      open1() {
        this.$message({
          showClose: true,
          message: '这是一条消息提示'
        });
      },

      open2() {
        this.$message({
          showClose: true,
          message: '恭喜你,这是一条成功消息',
          type: 'success'
        });
      },

      open3() {
        this.$message({
          showClose: true,
          message: '警告哦,这是一条警告消息',
          type: 'warning'
        });
      },

      open4() {
        this.$message({
          showClose: true,
          message: '错了哦,这是一条错误消息',
          type: 'error'
        });
      },
      
      // -------------------------------------------
      open5(){
        this.$message.error('错了哦,这是一条错误消息');
      }
        
    }
  }
</script>
  1. 对上述用例的分析:

a. this.message:可以看出message:可以看出message是Message组件的构造函数,并且element-ui把$message挂在了Vue的原型对象上,所以才能用this来调用。

b. 构造函数$message在new的过程中,传的参可以是String类型,也可以是VNode类型,还可以是对象类型.

c. Message具体的构造函数看源码packages/message/src/main.js里的:

const Message = function(options){ }

d. Message 里面有new实例化的关键一步:

instance = new MessageConstructor({
    data: options
 });

\

二、源码

源码来源:github.com/ElemeFE/ele…

在线看源码:github1s.com/ElemeFE/ele…

源码如下:

  1. 结构:packages/message

\

  1. 具体源码:

(1)packages/message/src/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: '',
        duration: 3000,
        type: 'info',
        iconClass: '',
        customClass: '',
        onClose: null,
        showClose: false,
        closed: false,
        verticalOffset: 20,
        timer: null,
        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: {
      closed(newVal) {
        if (newVal) {
          this.visible = false;
        }
      }
    },

    methods: {
      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);
        }
      },
      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>

以上代码涉及到的点:

a. transition: Vue使用transition来实现过渡动画

name="el-message-fade",也就是在设置动画的类名前加el-message-fade。

(截图为packages/theme-chalk/src/message.scss里面的部分代码)

b. role属性:是一种语义化的表现,除了上述源码中的role="alert"之外,还可以是role="form",role="button"等。

参考:HTML 中 “role” 属性的目的是什么?

在HTML5之前,只能通过role属性来定义landmark

c. 消息弹框的停留时间:

点击后默认显示3秒再自动关闭——在mouted阶段调用this.startTimer(),里面开了定时器去定时关闭;

esc键可以关闭弹框——在mouted阶段绑定keydown事件:document.addEventListener('keydown', this.keydown);

如果鼠标移入到消息弹框里,弹框显示时长延长3秒:元素上绑定了mouseenter事件;

如果鼠标移出消息弹框,3秒后弹框关闭:在元素上绑定了mouseleave事件。

d. 关闭的逻辑:

    • showClose:用这个变量来控制有没有关闭按钮。

具体从哪里传的,又是怎么接收的,看下面:

showClose作为参数对象的属性传进来,在Message内部是用vue的data的showClose来接收。

    • close函数与closed属性值:关闭按钮上绑定了点击事件,一点击就会触发close函数,在close函数里面把属性closed的值设置为true,然后watch监听了closed的更新,一被更改,就把visible的值设置为false,隐藏掉消息弹框;close函数里还调用了onClose函数——这里处理关闭后执行的操作。

elementUi中的onClose的关闭事件

    • onClose:element-ui的关闭事件,即关闭后执行的操作放这里。onClose的主要逻辑看packages/message/src/main.js。

所以main.vue里的this.onClose(this),实际调用的是main.js里的userOnClose(instances[i]):

(2)packages/message/src/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';
import { isObject } from 'element-ui/src/utils/types';

// 消息构造器
let MessageConstructor = Vue.extend(Main);  

let instance;
let instances = [];
let seed = 1;

const Message = function(options) {
  if (Vue.prototype.$isServer) return;
  options = options || {};
  if (typeof options === 'string') {
    options = {
      message: options
    };
  }
  let userOnClose = options.onClose;
  let id = 'message_' + seed++;

  options.onClose = function() {
    Message.close(id, userOnClose);
  };
  instance = new MessageConstructor({
    data: options
  });
  instance.id = id;
  if (isVNode(instance.message)) {
    instance.$slots.default = [instance.message];
    instance.message = null;
  }
  instance.$mount();
  document.body.appendChild(instance.$el);
  let verticalOffset = options.offset || 20;
  instances.forEach(item => {
    // 每个Message实例离页面顶部的垂直距离在上一个message弹框的基础上加上16px.
    verticalOffset += item.$el.offsetHeight + 16;  
  });
  instance.verticalOffset = verticalOffset;
  instance.visible = true;
  instance.$el.style.zIndex = PopupManager.nextZIndex();
  instances.push(instance);
  return instance;
};

['success', 'warning', 'info', 'error'].forEach(type => {
  Message[type] = (options) => {
    if (isObject(options) && !isVNode(options)) {
      return Message({
        ...options,
        type
      });
    }
    return Message({
      type,
      message: options
    });
  };
});

Message.close = function(id, userOnClose) {
  let len = instances.length;  // message弹框的个数
  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;

a. Message实例化:定义Message函数

      1. 构造一个消息构造器MessageConstructor:let MessageConstructor = Vue.extend(Main);
      2. 对传来的参数options做处理:
this.$message("这是一条消息!");

传来的参数用main.js的options来接收,当参数是String类型的时候,把options变成对象。也就是:

"这是一条消息!" ==> {message: "这是一条消息!"}

      1. new一个MessageConstructor: instance
instance = new MessageConstructor({
    data: options
});
      1. instance.$mount();
      2. Vue.extend + vm.$mount组合使用:Vue中 Vue.extend() 详解及使用
      3. 初始化一些属性值:比如处理传消息message属性传的是VNode类型时要做一些处理(插槽、把message置为null),还要设置instance的id、verticalOffset、visible等属性值。

b. this.$message.success( / warning / info / error):

this.$message.success("这是一条成功消息!")能起作用,主要是Message里面定义了success、warning、info、error四个方法,方法里面对参数做了是字符串还是对象、VNode的判断,再调用了Message函数——创建了实例。

c. 在Message里面定义了close方法和closeAll方法。 其中close方法里面,既处理了关闭消息的弹框的逻辑,又处理了用户使用在关闭弹框后的一些操作——即:关闭后的回调函数.

使用举例:

/* 
    message:提示信息
    offset:  距离窗口顶部偏移量
    duration: 显示时间,设置0时,不会自动关闭
    onClose:  回调函数
*/
this.$message.success({ 
    message: "审核成功",
    offset:220, 
    duration: 2000, 
    onClose: () => {
      this.$router.go(-1)    // 消息弹框关闭后,页面回到之前一个。
    } 
})

\

(3)css:packages/theme-chalk/src/message.scss:

@import "mixins/mixins";
@import "common/var";

@include b(message) {
  min-width: $--message-min-width;
  box-sizing: border-box;
  border-radius: $--border-radius-base;
  border-width: $--border-width-base;
  border-style: $--border-style-base;
  border-color: $--border-color-lighter;
  position: fixed;
  left: 50%;
  top: 20px;
  transform: translateX(-50%);
  background-color: $--message-background-color;
  transition: opacity 0.3s, transform .4s, top 0.4s;
  overflow: hidden;
  padding: $--message-padding;
  display: flex;
  align-items: center;

  @include when(center) {
    justify-content: center;
  }

  @include when(closable) {
    .el-message__content {
      padding-right: 16px;
    }
  }

  p {
    margin: 0;
  }

  @include m(info) {
    .el-message__content {
      color: $--message-info-font-color;
    }
  }

  @include m(success) {
    background-color: $--color-success-lighter;
    border-color: $--color-success-light;

    .el-message__content {
      color: $--message-success-font-color;
    }
  }

  @include m(warning) {
    background-color: $--color-warning-lighter;
    border-color: $--color-warning-light;

    .el-message__content {
      color: $--message-warning-font-color;
    }
  }

  @include m(error) {
    background-color: $--color-danger-lighter;
    border-color: $--color-danger-light;

    .el-message__content {
      color: $--message-danger-font-color;
    }
  }

  @include e(icon) {
    margin-right: 10px;
  }

  @include e(content) {
    padding: 0;
    font-size: 14px;
    line-height: 1;
    &:focus {
      outline-width: 0;
    }
  }

  @include e(closeBtn) {
    position: absolute;
    top: 50%;
    right: 15px;
    transform: translateY(-50%);
    cursor: pointer;
    color: $--message-close-icon-color;
    font-size: $--message-close-size;

    &:focus {
      outline-width: 0;
    }
    &:hover {
      color: $--message-close-hover-color;
    }
  }

  & .el-icon-success {
    color: $--message-success-font-color;
  }

  & .el-icon-error {
    color: $--message-danger-font-color;
  }

  & .el-icon-info {
    color: $--message-info-font-color;
  }

  & .el-icon-warning {
    color: $--message-warning-font-color;
  }
}

.el-message-fade-enter,
.el-message-fade-leave-active {
  opacity: 0;
  transform: translate(-50%, -100%);
}

消息弹框的样式:用了mixins和scss。

先来学习下mixins,看下面的资料:

a. scss + mixins: scss混合”mixins“使用

b. Element scss mixins 源码学习:

element scss mixin BEM 实现

element-ui源码阅读-样式

element-ui源码解读-基于scss的bem方法的实现

src/mixins/emitter.js中有两个方法:dispatch和broadcast

再来学习scss:

scss是什么:Sass是成熟、稳定、强大的CSS预处理器,而SCSS是Sass3版本当中引入的新语法特性,完全兼容CSS3的同时继承了Sass强大的动态功能。

Sass中文网(香港,需要科学上网)

官方教程太多了,一时半会看不完,那就看下面的快速上手吧!

听说你还不会SCSS?带你掌握scss所有知识点(1):Sass的介绍、安装和文件编译

听说你还不会SCSS?带你掌握scss所有知识点(2):掌握Scss基础语法和使用、局部文件导入

\

(4)packages/message/index.js:

import Message from './src/main.js';
export default Message;

用了ES6的模块化导入导出。

(5)src/index.js:

把封装好的message组件挂在Vue的原型上,以后就可以用this进行全局调用

源码如下,我删掉了element-ui的组件,留下了与Message相似的MessageBox和Notification:

/* Automatically generated by './build/bin/build-entry.js' */


import Message from '../packages/message/index.js';
import MessageBox from '../packages/message-box/index.js';
import Notification from '../packages/notification/index.js';

import locale from 'element-ui/src/locale';
import CollapseTransition from 'element-ui/src/transitions/collapse-transition';

const components = [
  
];

const install = function(Vue, opts = {}) {
  locale.use(opts.locale);
  locale.i18n(opts.i18n);

  components.forEach(component => {
    Vue.component(component.name, component);
  });

  Vue.use(InfiniteScroll);
  Vue.use(Loading.directive);

  Vue.prototype.$ELEMENT = {
    size: opts.size || '',
    zIndex: opts.zIndex || 2000
  };

  Vue.prototype.$loading = Loading.service;
  Vue.prototype.$msgbox = MessageBox;
  Vue.prototype.$alert = MessageBox.alert;
  Vue.prototype.$confirm = MessageBox.confirm;
  Vue.prototype.$prompt = MessageBox.prompt;
  Vue.prototype.$notify = Notification;
  Vue.prototype.$message = Message;

};

/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue);
}

export default {
  version: '2.15.8',
  locale: locale.use,
  i18n: locale.i18n,
  install,
  CollapseTransition,
  Loading,
  
  Message,
  MessageBox,
  Notification
};

把Message挂在Vue的原型对象prototype上。

以后就可以在项目全局使用this.$message( ) 触发消息弹框了,具体使用用例看最上面举的element-ui官方的例子,参数可以传字符串,可以传VNode,也可以传对象。

三、总结:

this.$message("这是一条消息");

this.$message({
  showClose: true,
  message: '恭喜你,这是一条成功消息',
  type: 'success'
});
  1. this.$message(),底层是调用了message函数,message函数里有new实例化的过程:把message的vue文件导出为Main对象,用Vue.extend(Main)构造一个构造器MessageConstructor。在message函数里就是new了MessageConstructor;
  2. this.$message()带的参数,在Element-UI源码里的message()函数也有处理,讨论了三种类型的参数:字符串、VNode、对象。其中对象的属性根据Element-UI的文档可以传message、type、offset、duration、showClose、onClose回调函数等;
  3. main.js里面在定义Message的success、info、error、warning四个方法时,有调用Message函数:Message(),说明this.$message.success(...) 这种使用方法,是Element自己调用的Message函数来生成一个实例。
  4. 但是针对Message本身,main.js里面只有对Message函数的定义,没有调用。说明this.$message是由Vue来调用Message函数从而生成message实例的。
  5. 关闭message实例的场景有两种:

(1)点击页面上消息弹框的关闭按钮;

(2)手动关闭:比如不是点击页面某个元素触发消息弹框的显示,而是在发请求给后端后需要显示消息弹框,此时想自己手动关闭,用close方法——this.$message.close()

也就是:
main.vue里面的methods里的close方法是给点击关闭按钮用的,
main.js里的Message.close方法是给手动关闭用的。

四、推荐阅读:

ElementUI的结构与源码研究

感谢阅读~

完~