跟着element学组件-message消息框

1,374 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、引言


大家好,我是王淼。最近也是了解了一些element-ui框架的源码,其中一些设计方式以及思路也是非常值得借鉴,我也是想和大家分享一下共同进步,让我们的组件更加优雅。

二、准备工作


  • 在看element-ui组件源码之前呢,我们要先了解Vue的一个全局方法Vue.extend

  • element-ui源码,如果访问不了可以在项目依赖中找到element-ui/packages/message进行阅读。

三、正文


3.1 Vue.extend

  • 参数
-   `{Object} options`
  • 用法

    使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

    data 选项是特例,需要注意 - 在 Vue.extend() 中它必须是函数

    <div id="mount-point"></div>
    
    // 创建构造器
    var Profile = Vue.extend({
      template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
      data: function () {
        return {
          firstName: 'Walter',
          lastName: 'White',
          alias: 'Heisenberg'
        }
      }
    })
    // 创建 Profile 实例,并挂载到一个元素上。
    new Profile().$mount('#mount-point')
    

       上述为官方对extend方法的说明,表述的可能不是很详细,用我个人的描述来说就是可以将vue模板渲染到任意的dom元素中,其中options参数参数可以像官方演示中那样传入,也可以通过import引入Vue模板。

import Main from './main.vue';
let Profile = Vue.extend(Main);
new Profile().$mount('#mount-point')

       在创建Profile实例的过程中也是可以传入参数的,此参数为引入实例的补充参数,Vue会将传入参数以及模板进行合并。

import Main from './main.vue';
let Profile = Vue.extend(Main);
const templateExtend = {
    data:{
        id:1
    }
}
new Profile(templateExtend).$mount('#mount-point')

       进行mount挂载操作的时候,其参数可以选择性传入,如果没有传入的话需要手动将实例中的$el添加元素到dom。

import Main from './main.vue';
let Profile = Vue.extend(Main);
const instance = new Profile().$mount()
document.body.appendChild(instance.$el)

以上就是extend的参数以及使用方法。

3.2 element-ui

      当我们进入element-ui的element-ui/packages目录的时候,其实可以发现所有的组件目录结构都如下图所示

.
├── index.js       //将组件导出
└── src
    ├── main.js    //实例创建 && 一些操作逻辑
    └── main.vue   //message组件的template
3.2.1 main.js文件的结构
import Vue from 'vue';
import Main from './main.vue';
let MessageConstructor = Vue.extend(Main);
let instance;
let instances = [];
let seed = 1;
const Message = function(options){}   //创建message实例
Message.close = function(seed,userOnClose){}  //销毁某个message实例 seed为唯一id
Message.closeAll = function(){}   //销毁所有message实例
export default Message

可以看出Message组件也是非常简洁,只有创建和销毁的操作。下面我们就来看看里面到底是个什么样子(代码为简化代码,部分判断逻辑移除)

Message

function(options){
  let userOnClose = options.onClose;  //传入close方法
  let id = 'message_' + seed++;      //每一个实例独有ID
  options.onClose = function() {     //实例关闭方法
    Message.close(id, userOnClose);
  };
  instance = new MessageConstructor({
    data: options
  });
  instance.id = id;
  instance.$mount();   //实例化
  document.body.appendChild(instance.$el);  //加入dom
  let verticalOffset = options.offset || 20; //消息弹窗高度
  instances.forEach(item => {
    verticalOffset += item.$el.offsetHeight + 16;
  });
  instance.verticalOffset = verticalOffset;
  instance.visible = true;
  instances.push(instance);
  return instance;
}

代码解析:进入方法后会给当前实例创建一个唯一ID(通过唯一ID进行移除),接着把用户传进来的关闭回调传递给Message.close,在将$el追加到dom之后再将verticalOffset和visible挂载到了实例中。

verticalOffset:在实例化完成之后根据已有的所有实例将当前实例应该在的高度计算出来,这样就能在模板中使用this.verticalOffset设置top值来实现页面中多个message按序排列显示。

visible:在模板中通过this.visible进行显示隐藏,并配合vue-transition进行渐变效果。 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';
  }
};

代码解析:这一块的主要逻辑主要是通过id找出需要移除的实例,判断用户是否传入自定义userClose事件并调用该事件,通过循环instances将】调整所有未关闭的实例的高度。 Message.closeAll

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

代码解析:循环s实例列表,调用close方法

3.2.2 main.vue模板结构(简化)
<template>
  <transition name="el-message-fade" @after-leave="handleAfterLeave">
    <div:class="['el-message']" v-show="visible">
    </div>
  </transition>
</template>

<script type="text/babel">
  export default {
    data() {
      return {
        visible: false,
        message: '',
        duration: 3000,
        onClose: null,
        verticalOffset: 20,
        timer: null,
      };
    },

    computed: {
      positionStyle() {
        return {
          'top': `${ this.verticalOffset }px`
        };
      }
    },
    methods: {
      handleAfterLeave() {
        this.$destroy(true);
        this.$el.parentNode.removeChild(this.$el);
      },
      close() {
        this.visible = false;
        this.onClose(this);
      },

      clearTimer() {
        clearTimeout(this.timer);
      },
      startTimer() {
        if (this.duration > 0) {
          this.timer = setTimeout(() => {
              this.close();
          }, this.duration);
        }
      },
    },
    mounted() {
      this.startTimer();
    },
  };
</script>

代码解析:在组件创建之后调用startTimer方法,三秒后执行close方法将visible设置为false,并且调用main.js中传入的onClose方法将组件移出数组,设置其他实例的高度。在visible为false并且动画执行完成后会调用handleAfterLeave方法将当前dom移出。

当然我们也可以把setTimeout放在main.js,通过Promise的特性将resolve挂载到实例中完成此功能呢~

四、感想

      今天主要分享了Vue.extned方法的使用,全局组件的实现思路。当然,这也只是实现全局组件的其中一种方式,还有很多实用的技巧需要去探索,以后也会跟大家分享其他组件的实现思路哒。

      我也是第一次写技术文章,语言描述能力有些薄弱QAQ,文章中有遗漏或者不足的地方希望大家能够指出来呀,如果觉得有用,不要忘了点个赞呦~

听说喜欢点赞的你,今年年终奖拿到手软😍