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

1,022 阅读3分钟

简介

本文将介绍组件服务方式的实现,耐心读完,相信会对您有所帮助。 为了更好的理解本文,请先阅读下前文Message消息提示(上)-组件实现

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

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

源码结构

组件服务方式实现的源码文件为 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)

image.png

数组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,重新计算偏移量。

下图展现了关闭页面第二个实例后,随后的二个实例的偏移量需要重新计算。

image.png

组件的关闭流程

现在将各功能点串起来,解释下组件关闭时,发生了什么?

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

即使用户没有传入关闭时回调函数,组件实例创建时也会有方法传入,用于组件关闭销毁后更新整个页面实例数组更新剩余实例偏移量。

// packages\message\src\main.js
const 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

关注专栏

如果本文对您有所帮助请关注➕、 点赞👍、 收藏⭐!您的认可就是对我的最大支持!

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