简介
音讯提醒组件 Message
罕用于被动操作后的反馈提醒,顶部居中显示并主动隐没,是一种不打断用户操作的轻量级提醒形式。本文将剖析其源码实现,急躁读完,置信会对您有所帮忙。🔗 组件文档 Message 🔗 gitee 源码
更多组件分析详见 👉 📚 Element 2 源码分析组件总览。
应用形式
组件 Message
以服务的形式调用。Message
组件入口文件中没有,没有插件申明,只是导出了办法 Message
;在组件库入口文件中,将办法 Message
增加至 Vue.prototype
。
// `Message` 组件入口文件
// packages\message\index.js
import Message from './src/main.js';
export default Message;
在组件库入口文件也是一样的解决。
// 组件库入口
// src/index.js
import Message from '../packages/message/index.js';
//...
const install = function(Vue, opts = {}) {
//...
Vue.prototype.$message = Message;
};
export default {
//...
Message,
};
调用办法为 Message(options)
。组件也为每个 type 定义了各自的办法,如 Message.success(options)
。调用 Message.closeAll()
手动敞开所有实例。组件库残缺引入,间接应用this.$message(options)
。
// 残缺引入
this.$message(options);
// 独自援用
import {Message} from 'element-ui';
// ...
Message(options);
其中 options
参数为 Message
的配置项,在此不做详尽解释, 详见 组件文档 Message #options。
组件源码
HTML
音讯提醒组件页面元素构造比较简单。
根节点下元素依照功辨别,次要有三局部:
- Icon 图标
- 音讯文字
- 敞开按钮
应用 transition
组件,在组件根节点的条件展现 (v-show
)中增加过渡成果, 定义了钩子函数after-leave
用于设置过渡来到实现之后的组件状态。
// packages\message\src\main.vue
<template>
<!-- transition 过渡组件,绑定 after-leave 钩子 -->
<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" >
<!-- 主题 / 自定义图标 -->
<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>
// 主题类型图标映射
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, // 间隔顶部的偏移 top: 20px
timer: null, // 定时器,管制组件主动敞开
dangerouslyUseHTMLString: false, // 是否将 message 属性作为 HTML 片段解决
center: false // 文字是否居中
};
},
computed: {
// 不同主题 type 的图标
typeClass() {
return this.type && !this.iconClass
? `el-message__icon el-icon-${typeMap[this.type] }`
: '';
},
// 设置 top
positionStyle() {
return {'top': `${ this.verticalOffset}px`
};
}
},
// ...
};
</script>
top 偏移量
元素根节点是一个类名 el-message
的 div 元素,应用相对布局。fixed
示意脱离文档流,通过指定元素绝对于屏幕视口(viewport)的地位来指定元素地位,程度方向居中,垂直方向居上。
.el-message {
position: fixed;
left: 50%;
top: 20px;
}
应用计算属性 positionStyle
基于设置的 verticalOffset
属性值动静计算组件间隔顶部的偏移量。
// top: 20px
positionStyle() {
return {'top': `${ this.verticalOffset}px`
};
}
页面中能够存在多个 Message
实例,新 Message 音讯会在旧的上面展现,此时实例依据在数组中所处索引值,计算出实例的间隔顶部的偏移量 verticalOffset
。组件实例随着主动 / 人工敞开销毁,数组内容变量,其索引值会变动,verticalOffset
值也会从新计算,下文“服务实现”一节中会具体介绍该逻辑。
状态主题
状态主题属性 type
默认值 info
, 组件反对 success/warning/info/error
共四种可选值。
根节点中基于 type
值生成不同主题款式 el-message--[success/warning/info/error]
。但当传入属性iconClass
值用于自定义图标的类名,就不会生成主题款式,此时 type
设置有效。
type && !iconClass ? `el-message--${type}` : '',
子元素内容布局
message
组件外部应用 flex 布局。属性 center
用于生成类名 is-center
设置图标和音讯文字居中。
.el-message {
display: flex;
align-items: center;
}
.el-message.is-center {justify-content: center;}
敞开图标应用相对布局,垂直居中程度居右。
<!-- 敞开图标 -->
<i v-if="showClose" class="el-message__closeBtn el-icon-close" @click="close"></i>
.el-message__closeBtn {
position: absolute;
top: 50%;
right: 15px;
}
过后显示敞开图标时,会生成类名is-closable
,避免音讯文字跟敞开按钮由重叠。
.el-message.is-closable .el-message__content {padding-right: 16px;}
Icon 图标优先显示自定义。主题图标的类名应用计算属性typeClass
。
<!-- 主题 / 自定义图标 -->
<i :class="iconClass" v-if="iconClass"></i>
<i :class="typeClass" v-else></i>
音讯文字插槽
属性 message
反对传入 HTML 片段,然而须要显示关上此性能(将属性dangerouslyUseHTMLString
设置 true)。
<!-- 默认插槽 -->
<slot>
<p v-if="!dangerouslyUseHTMLString" class="el-message__content">{{message}}</p>
<p v-else v-html="message" class="el-message__content"></p>
</slot>
当属性 message
传入值类型为 VNode
时,会应用插槽性能,下文“服务实现”一节会具体介绍。
// packages\message\src\main.js
if (isVNode(instance.message)) {instance.$slots.default = [instance.message];
instance.message = null;
}
在网站上动静渲染任意 HTML 是十分危险的,因为容易导致 XSS 攻打。因而在
dangerouslyUseHTMLString
关上的状况下,请确保message
的内容是可信的,永远不要 将用户提交的内容赋值给message
属性。
生命周期 & 事件
组件被挂载之后调用办法 startTimer
启用定时器,实现 message 实例的主动敞开。在办法 startTimer
中当属性 duration
值大于 0 时(if (this.duration > 0)
),才会创立定时器用于主动敞开;若组件不须要主动敞开,将属性 duration
值设置为 0
即可。
挂载之后增加 keydown
事件监听。实例销毁之前, 会移除 keydown
事件监听。办法 keydown
用于实现按 ESC
键敞开音讯组件。如果页面存在多个实例,会全副敞开。
应用自定义侦听器,当属性 closed
值变动且为 true 时,将属性 visible
值置为 false(组件暗藏)。
export default {
// ...
// 挂载时
mounted() {
// 启动定时器
this.startTimer();
// 监听 keydown 事件
document.addEventListener('keydown', this.keydown);
},
beforeDestroy() {
// 勾销 keydown 监听
document.removeEventListener('keydown', this.keydown);
},
watch: {closed(newVal) {if (newVal) {this.visible = false;}
}
},
methods: {
// 组件敞开事件
close() {// ...},
// 过渡 `after-leave` 钩子函数
handleAfterLeave() {// ...},
// 革除定时器,当 mouseenter 时调用
clearTimer() {clearTimeout(this.timer);
},
// 启动定时器,duration 默认是 3s,到工夫自动隐藏
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();
}
}
}
},
};
根节点绑定 mouseenter
、mouseleave
事件,当鼠标挪动到 message 实例上,会革除其定时器clearTimer
,该实例就不会主动敞开。当鼠标移出后,会从新创立定时器startTimer
,实现主动敞开。
<div @mouseenter="clearTimer" @mouseleave="startTimer" >
// ...
</div>
办法 close
用于敞开组件,如果用户设置了属性 onClose
值,敞开时会执行该回调函数, 参数为被敞开的 message 实例。
close() {
this.closed = true;
if (typeof this.onClose === 'function') {this.onClose(this);
}
},
当组件敞开后,会触发 transition
组件绑定after-leave
钩子函数,执行办法 handleAfterLeave
。过渡来到实现之后,执行办法vm.$destroy()
,齐全销毁该实例,触发 beforeDestroy
的钩子;同时将实例 DOM 元素从页面移除
handleAfterLeave() {
// 齐全销毁一个实例。清理它与其它实例的连贯,解绑它的全副指令及事件监听器
// 触发 beforeDestroy 和 destroyed 的钩子
this.$destroy(true);
this.$el.parentNode.removeChild(this.$el);
},
服务形式源码
组件服务形式实现的源码文件为 packages\message\src\main.js
。
源码精简后构造如下,代码创立了 function
类型的对象Message
,同时给对象增加属性办法 close
、closeAll
、warning
、info
、error
,导出对象 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
。该函数用于初始化配置创立组件,并返回该实例。
函数实现性能次要有以下步骤:
- 参数
options
初始化。 - 应用
Vue.extend
、vm.$mount()
创立渲染挂载实例,默认将其增加至 body 元素节点下。 - 依据数组
instances
中实例数量,计算并设置该实例顶部的垂直偏移。 - 将实例设置显示可见
visible = true
。 - 更新数组
instances
,将该实例增加至其中。 - 返回
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)
数组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';
}
};
办法实现性能次要有以下步骤:
- 依据组件 id 从数组中查找实例的索引 index。
- 未找到对应实例,匹配条件
index === -1
,退出办法。 -
若找到对应实例,记录实例索引 index。
- 获取实例元素 offsetHeight。
- 调用用户传入的敞开时回调函数。
- 从数组 instances 中去掉移除该实例。
- 判断该索引后是否还有其余元素,没有的话,退出办法;有的话执行下一步。
- 只调整 index 大于以后 Message 的实例的高度,也就是实例之后的实例元素。
- 依据移除实例元素 offsetHeight 和 间距 16,从新计算偏移量。
下图展示了敞开页面第二个实例后,随后的二个实例的偏移量须要从新计算。
组件的敞开流程
当初将各性能点串起来,解释下组件敞开时,产生了什么?
对象 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
应用混合指令 b
、when
、m
、e
嵌套生成组件款式。
// 生成 .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
关注专栏
如果本文对您有所帮忙请关注➕、点赞👍、珍藏⭐!您的认可就是对我的最大反对!