前言
element-业界驰名
自己准备通过这一系列文章,将element2-message、element Plus等ui框架源码的分析分享出来,把Vue2、Vue3基础、TypeScript、jest、monorepo项目管理模式等知识点串联一遍,熟悉一下。
结构分析
github上clone项目之后,packages/message便是咱们的目标目录。
目录很简单,index.js引入main.js之后,暴露出来,然后在人口文件中引入,然后以别名$message形式挂载在Vue.prototype上。
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: "",
// 显示时间, 毫秒。设为 0 则不会自动关闭
duration: 3000,
// 类型:success/warning/info/error
type: "info",
// 自定义class-icon类名
iconClass: "",
// 自定义类名
customClass: "",
// 关闭时的回调函数, 参数为被关闭的 message 实例
onClose: null,
// 是否显示关闭按钮
showClose: false,
// 本组件用于判断结束显示的标志
closed: false,
// 高度,用于多个实例同时出现时,往下加一定的高度
verticalOffset: 20,
// 定时器,三秒关闭这个实例
timer: null,
// 是否将 message 属性作为 HTML 片段处理
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: {
// 为什么要多立个flag?
closed(newVal) {
if (newVal) {
this.visible = false;
}
},
},
methods: {
// 动画执行完销毁el
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);
}
},
// 监听ESC键
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>
上面代码是main.vue的源码,清晰明了。之前没有在意过一个小功能就是鼠标放上去,实例会一直显示,代码中通过@mouseenter="clearTimer"来实现的。还有this.$destroy(true),这个参数true在Vue2不用加,感谢倔友的分享。
顺便分享下Vue2中关于$destroy的源码
可以看出$destory中没有接收参数,并在函数中依次调用了beforeDestroy、destroyed方法。
main.js
相比main.vue,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';
let MessageConstructor = Vue.extend(Main);
// MessageConstructor实例
let instance;
let instances = [];
// 用来给instances数组中每个实例设置id
let seed = 1;
const Message = function(options) {
// 服务端不得用哦!
if (Vue.prototype.$isServer) return;
// 调用this.$message时传递进来的参数
options = options || {};
// 如果是this.$message("测试弹出")这样的,可以直接转换
if (typeof options === 'string') {
options = {
message: options
};
}
// 关闭回调
let userOnClose = options.onClose;
// 实例标识
let id = 'message_' + seed++;
// 添加关闭回调
options.onClose = function() {
Message.close(id, userOnClose);
};
// 创建message实例
instance = new MessageConstructor({
data: options
});
instance.id = id;
// 接收render函数
if (isVNode(instance.message)) {
// https://cn.vuejs.org/v2/guide/render-function.html?
// 默认插槽接收一个节点数组
instance.$slots.default = [instance.message];
instance.message = null;
}
// 手动挂载
instance.$mount();
document.body.appendChild(instance.$el);
// 多个实例的时候,逐个下沉16px
let verticalOffset = options.offset || 20;
instances.forEach(item => {
verticalOffset += item.$el.offsetHeight + 16;
});
instance.verticalOffset = verticalOffset;
// 展示实例
instance.visible = true;
// 处理z-index
instance.$el.style.zIndex = PopupManager.nextZIndex();
// 放入实例数组中
instances.push(instance);
return instance;
};
// 取别名
['success', 'warning', 'info', 'error'].forEach(type => {
Message[type] = options => {
if (typeof options === 'string') {
options = {
message: options
};
}
options.type = type;
return 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;
关于main.js的逻辑是:定义Message函数-->取别名-->添加close(删除某个实例的方法)/closeAll方法;Message函数的逻辑是:判断赋值options-->添加关闭回调-->创建MessageConstructor实例-->赋值实例id-->手动挂载-->ui响应-->展示实例-->放入实例数组
开头引用的PopupManager是用来设置z-index的,具体可以参考这篇文章 关于使用element中的popup问题 ;引入的VNode方法是为了判断节点是否是render函数;
关于Vue.extend在平时做Vue项目时用的不多,所以这次正好复习了这个知识点。Vue.extend()
从Vue.extend的源码中可以知道,在合并extendOptions的时候,并没有解除其映射,所以经过如下操作之后,options同该实例中的data映射的是同一个地址。通过打印options就可以看出来。
关于手动挂载:Vue.$mount() ;使用 vm.$mount() 手动地挂载一个未挂载的实例,如果没有提供 elementOrSelector 参数,模板将被渲染为文档之外的的元素,并且你必须使用原生 DOM API 把它插入文档中。
阅读源码是一种很好的学习途径,写这篇文章也是为了记录分享一下自己的心得体会,希望各位大佬多多指教。相关的源码,已经上传至github,请各位自行查阅。element-source-code