likes
comments
collection
share

以el-message为例分析如何编写一个Message组件

作者站长头像
站长
· 阅读数 42

本文分析了element-ui的Message组件源码并介绍了如何单例地使用Message组件。

github地址: https://github.com/ElemeFE/element.git
message组件位置:element/packages/message

1.main.js文件

源码不到100行,整体思路比较清晰,建议先大致看一下,然后看完分析再回来看这大段的代码:

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 => {
    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;
  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;

有如下需要关注的地方

1.1Message方法

let MessageConstructor = Vue.extend(Main);

Vue.extend用于创建一个子类,详见: cn.vuejs.org/v2/api/#Vue…

const Message = function(options) {}

函数Message是一个构造函数,在构造函数内部会判断Vue是否运行于服务器,如果是的话则停止构造过程:

if (Vue.prototype.$isServer) return;

接着判断options参数的类型对其进行规范化:

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

options参数如果是字符串则赋值给对象的message字段。因为可以有如下的调用方式:

this.$message('这是一条消息提示');

接下来重新定义options的onClose,这是关闭时的回调,调用Message.close方法时会触发onClose:

以el-message为例分析如何编写一个Message组件

let userOnClose = options.onClose;
let id = 'message_' + seed++;

options.onClose = function() {
  Message.close(id, userOnClose);
};

加下来是创建Message实例的过程:

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

创建实例并且绑定了id属性。

 if (isVNode(instance.message)) {
   instance.$slots.default = [instance.message];
   instance.message = null;
 }

message字段是要展示的内容,但是可以是vnode,如果是这种情况则将message赋值给实例的默认插槽,在实际开发中是可以这么调用的:

const h = this.$createElement;
  this.$message({
    message: h('p', null, [
      h('span', null, '内容可以是 '),
      h('i', { style: 'color: teal' }, 'VNode')
    ])
});

接下来是要让Message组件显示出来:

instance.$mount();
document.body.appendChild(instance.$el);
let verticalOffset = options.offset || 20;
instances.forEach(item => {
  verticalOffset += item.$el.offsetHeight + 16;
});
instance.verticalOffset = verticalOffset;
instance.visible = true;
instance.$el.style.zIndex = PopupManager.nextZIndex();
instances.push(instance);
return instance;

首先使用$mount方法(详见:cn.vuejs.org/v2/api/#vm-…)手动挂载组件,然后设为body的子节点,接着是对相关样式的设置(offsetHeight, visible, z-index),最后是实例放入instances数组中,因为默认可以同时显示多个Message组件,如下图所示:

以el-message为例分析如何编写一个Message组件

1.2 success,warning,info,error方法

接下来是在Message的基础上进行扩展,增加4个方法:success,warning,info,error

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

在使用时是这样调用的:

this.$message.success('恭喜你,这是一条成功消息');
this.$message.warning('警告哦,这是一条警告消息');
this.$message.info('这是一条消息提示');
this.$message.error('错了哦,这是一条错误消息');

代码运行效果如下图:

以el-message为例分析如何编写一个Message组件

1.3 close与closeAll方法

然后是对close方法的定义:

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';
  }
};

前面提到过close方法会触发onClose回调,所以这里onClose(userOnClose)作为函数参数传递进来。在方法中主要做两件事儿:(1)删除这个Message;(2)改其他Message的样式。

删除这个Message的时候先获取其高度(offsetHeight)留着之后更改它下面的Message组件样式时候使用;然后判读用户是否传了onClose回调函数,如果传了则调用;接着从instaces里面删除这个Message组件。

更改其他Message的样式主要是修改top值,在被删除Message组件下面的那些组件都要向上移动一段距离。

除了删除一了Message组件之外还可以删除所有的Message:

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

定义了closeAll方法,遍历instances数组依次调用每一Message组件的close方法。

2.main.vue

2.1 模板部分

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

整个Message的模板部分用内置组件transition进行了包裹,为其提供过渡效果。相关的样式定义的相对路径为packages/theme-chalk/src/message.scss, 内容为:

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

含义就是进入过渡的开始状态和离开过渡生效时的状态透明度设置为0,向左侧(x轴)移动自身长度的50%,向上侧(y轴)移动自身长度的100%,为了能够水平居中,上下方向居上显示。

接着是动态绑定一些样式,通过class和style实现样式的动态绑定。

绑定的事件有mouseenter和mouseleave。二者分别调用clearTimer和startTimer方法。startTimer是开始计时,一段时间后关闭Message组件而clearTimer用于清除定时。这从逻辑上讲是很合理的,因为鼠标移入往往表示用户比较关注这个Message,鼠标移出了表示不关注可以消失了。

接着是对消息文言之前的icon的定义:

以el-message为例分析如何编写一个Message组件

下面代码的含义是:如果用于传了想要显示的iconClass则优选使用,否则使用typeClass。

<i :class="iconClass" v-if="iconClass"></i>
<i :class="typeClass" v-else></i>

接下来就是显示内容了:要判断用户传的是普通字符串还是html字符串

<p v-if="!dangerouslyUseHTMLString" class="el-message__content">{{ message }}</p>
<p v-else v-html="message" class="el-message__content"></p>

以el-message为例分析如何编写一个Message组件

最后是判断是否要显示关闭按钮:

 <i v-if="showClose" class="el-message__closeBtn el-icon-close" @click="close"></i>

带关闭按钮的运行效果如下:

以el-message为例分析如何编写一个Message组件

2.2 JS部分

节选了部分JS的代码,如下:

<script type="text/babel">

  export default {
    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>

首先看mounted生命周期中的事情:在mounted后执行startTimer方法,一定时间之后关闭Message;然后注册keydown事件。keydown事件对应的方法也叫keydown,检查按键code是否为27(esc键),如果是27并且当前Message没有关闭则调用关闭方法关闭。在beforeDestroy生命周期中移除注册的事件。

startTimer和clearTimer还有close方法上面说过,不再赘述。handleAfterLeave()定义了过渡离开的事件,调用$destory(cn.vuejs.org/v2/api/#vm-…)手动销毁,然后从父组件中移除。

3.项目实际应用:单例使用

在我们的项目中要求同一时间只能弹出一个Messsage,也就是要求全局单例。

实现思路是通过document.getElementsByClassName('el-message')判断当前有没有message组件,如果没有则调用Message方法。具体定义见resetMessage.js:

/**重置message,防止重复点击重复弹出message弹框 */
import { Message } from 'element-ui'
function showMessage(type, options, single) {
  if (single) {
    if (document.getElementsByClassName('el-message').length === 0) {
      Message[type](options)
    }
  } else {
    Message[type](options)
  }
}
function init(options, single = true) {
  showMessage(options.type, options, single)
}
function DoneMessage(options) {
  if (options) {
    init(options, options.single)
  }
}
function info(options, single = true) {
  showMessage('info', options, single)
}
function warning(options, single = true) {
  showMessage('warning', options, single)
}
function error(options, single = true) {
  showMessage('error', options, single)
}
function success(options, single = true) {
  showMessage('success', options, single)
}
DoneMessage.info = info
DoneMessage.warning = warning
DoneMessage.error = error
DoneMessage.success = success
export const message = DoneMessage

首先定义了showMessage方法,接收三个参数分别是类型type、配置对象options、是否单例single。如果是单例则执行document.getElementsByClassName('el-message')判断当前有没有message组件,如果没有则调用Message方法,否则不调用;如果不要求单例则直接调用Message方法。

如果用户直接调用this.message则调用DoneMessage方法,在DoneMessage中调用init方法,init里调用了showMessage。接着定义了info,warning,error,success4个和类型有关的方法,里面都是调用showMessage,把这些方法定义为DoneMessage的属性,使用户可以通过this.message则调用DoneMessage方法,在DoneMessage中调用init方法,init里调用了showMessage。 接着定义了info, warning,error,success4个和类型有关的方法,里面都是调用showMessage,把这些方法定义为DoneMessage的属性,使用户可以通过this.messageDoneMessageDoneMessageinitinitshowMessageinfo,warning,error,success4showMessageDoneMessage使this.message.success()的方式调用。

看一下使用方法:

import { message } from './resetMessage.js'
Vue.prototype.$message = message


this.$message.success('保存成功')
this.$message({
  type: 'success',
  message: '回收成功!'
})

4.总结

element-ui的Message组件有如下特点:

Message可以是字符串也可以是vnode;

Message组件支持特定的消息类型;

Message组件弹出后隔一段时间后消失,也可以有可选的关闭按钮。

转载自:https://juejin.cn/post/7078657768571764744
评论
请登录