一、组件介绍
el-message
是一个信息提示类组件,常用于系统级别的提醒,在我的项目中,会使用el-message
组件展示用户的操作提示及操作结果,如:操作成功、操作失败:xxxx原因
。
el-message
的使用与一般的element-plus
组件有所不同,其不是通过在template
中使用标签的方式引入,而是在js代码中,通过api的方式进行调用。
使用示例:
ElMessage.success( '恭喜你,这是一条成功消息');
复制代码
二、源码分析
el-message
的源码有2部分:组件展示部分和api方法
2.1 组件展示部分 packages\components\message\src\index.vue
该部分使用vue文件,主要作用是展示消息内容
<template>
<!-- 过渡组件,绑定before-leave after-leave钩子 -->
<transition name="el-message-fade" @before-leave="onClose" @after-leave="$emit('destroy')">
<!-- 绑定mouseenter mouseleave事件,清除/启动定时器 -->
<div
v-show="visible"
:id="id"
:class="[
'el-message',
type && !iconClass ? `el-message--${type}` : '',
center ? 'is-center' : '',
showClose ? 'is-closable' : '',
customClass,
]"
:style="customStyle"
role="alert"
@mouseenter="clearTimer"
@mouseleave="startTimer"
>
<!-- 图标,根据类型展示,绑定传入的iconClass -->
<i v-if="type || iconClass" :class="['el-message__icon', typeClass, iconClass]"></i>
<!-- 默认插槽 -->
<slot>
<!-- 未开启dangerouslyUseHTMLString时,message作为文本内容展示 -->
<p v-if="!dangerouslyUseHTMLString" class="el-message__content">{{ message }}</p>
<!-- 开启dangerouslyUseHTMLString时,作为Html内容展示-->
<!-- 需要注意的是,v-html可能引起xss攻击,不要使用不信任的用户输入作为message -->
<!-- eslint-disable-next-line -->
<p v-else class="el-message__content" v-html="message"></p>
</slot>
<!-- 展示关闭图标 -->
<div v-if="showClose" class="el-message__closeBtn el-icon-close" @click.stop="close"></div>
</div>
</transition>
</template>
<script lang="ts">
import { defineComponent, computed, ref, onMounted, onBeforeUnmount } from 'vue'
import { EVENT_CODE } from '@element-plus/utils/aria'
import { on, off } from '@element-plus/utils/dom'
// MessageVM is an alias of vue.VNode
import type { PropType } from 'vue'
import type { Indexable } from '@element-plus/utils/types'
import type { MessageVM } from './types'
const TypeMap: Indexable<string> = {
success: 'success',
info: 'info',
warning: 'warning',
error: 'error',
}
export default defineComponent({
name: 'ElMessage',
props: {
// 自定义class name
customClass: { type: String, default: '' },
// 是否居中显示
center: { type: Boolean, default: false },
// 是否开启v-html功能
dangerouslyUseHTMLString: { type: Boolean, default: false },
// message显示的时间,默认3s
duration: { type: Number, default: 3000 },
// 提示图标的class,会覆盖默认的type class
iconClass: { type: String, default: '' },
// id
id: { type: String, default: '' },
// 消息内容,可以是string类型,或者是vnode
message: {
type: [String, Object] as PropType<string | MessageVM>,
default: '',
},
// 关闭时的回调函数
onClose: {
type: Function as PropType<() => void>,
required: true,
},
// 是否展示关闭图标
showClose: { type: Boolean, default: false },
// message的类型,success warning info error
type: { type: String, default: 'info' },
// 距离顶部的偏移
offset: { type: Number, default: 20 },
// zIndex值
zIndex: { type: Number, default: 0 },
},
// 向外发射的事件
emits: ['destroy'],
setup(props) {
// 图标使用的typeClass
const typeClass = computed(() => {
// 若props.iconClass有值,则返回''
const type = !props.iconClass && props.type
return type && TypeMap[type]
? `el-icon-${TypeMap[type]}`
: ''
})
// 动态style,设置top和zindex
const customStyle = computed(() => {
return {
top: `${props.offset}px`,
zIndex: props.zIndex,
}
})
const visible = ref(false)
let timer = null
// 启动定时器,duration默认是3s,到时间自动隐藏
function startTimer() {
if (props.duration > 0) {
timer = setTimeout(() => {
if (visible.value) {
close()
}
}, props.duration)
}
}
// 清除定时器,当mouseenter时调用
function clearTimer() {
clearTimeout(timer)
timer = null
}
function close() {
visible.value = false
}
// 监听键盘按键事件,
function keydown({ code }: KeyboardEvent) {
if (code === EVENT_CODE.esc) {
// esc按键,则关闭显示
if (visible.value) {
close()
}
} else {
// 其他按键,重启计时器
startTimer() // resume timer
}
}
// 挂载时
onMounted(() => {
// 启动定时器
startTimer()
// 设置内容可见
visible.value = true
// 在document上监听keydown事件
on(document, 'keydown', keydown)
})
// 卸载时
onBeforeUnmount(() => {
// 取消keydown监听
off(document, 'keydown', keydown)
})
return {
typeClass,
customStyle,
visible,
close,
clearTimer,
startTimer,
}
},
})
</script>
复制代码
2.2 Api部分 packages\components\message\src\message.ts
该部分作用是提供Api对外使用
import { createVNode, render } from "vue";
import { isVNode } from "@element-plus/utils/util";
import PopupManager from "@element-plus/utils/popup-manager";
import isServer from "@element-plus/utils/isServer";
import MessageConstructor from "./index.vue";
import type { ComponentPublicInstance } from "vue";
import type { IMessage, MessageQueue, IMessageOptions, MessageVM, IMessageHandle, MessageParams } from "./types";
// 全局变量,存储所有message实例,便于修改message实例的垂直offset
const instances: MessageQueue = [];
// 递增计数,新建实例时seed++
let seed = 1;
const Message: IMessage = function (opts: MessageParams = {} as MessageParams): IMessageHandle {
if (isServer) return;
// 传入的参数,可以是string类型
// string类型时,转换成对象形式
if (typeof opts === "string") {
opts = {
message: opts,
};
}
let options: IMessageOptions = <IMessageOptions>opts;
// 距离顶部的垂直偏移,默认是20px
let verticalOffset = opts.offset || 20;
// 新的Message弹框在旧的Message弹框下面展示
// 垂直偏移要加上当前已有的Message弹框的距离
instances.forEach(({ vm }) => {
verticalOffset += (vm.el.offsetHeight || 0) + 16;
});
verticalOffset += 16;
// 递增的唯一id
const id = "message_" + seed++;
// 用户传递的close时的回调函数
const userOnClose = options.onClose;
// 组装成新的options,作为message组件的props
options = {
...options,
onClose: () => {
close(id, userOnClose);
},
offset: verticalOffset,
id,
// 获得最新的zIndex
zIndex: PopupManager.nextZIndex(),
};
// 创建一个新的div元素,作为弹框的容器
const container = document.createElement("div");
container.className = `container_${id}`;
const message = options.message;
// MessageConstructor即message/src/index.vue,是message组件的构造器
// options作为message组件的props
// 如果options.message传入的是一个vnode,则将此vnode作为组件的children渲染
const vm = createVNode(MessageConstructor, options, isVNode(options.message) ? { default: () => message } : null);
// 效果等同于在组件上@destroy,trainsition组件的after-leave钩子会emit destroy,进而执行此方法
// 删除message组件,避免内存泄漏
vm.props.onDestroy = () => {
render(null, container);
// since the element is destroy, then the VNode should be collected by GC as well
// we do not want cause any mem leak because we have returned vm as a reference to users
// so that we manually set it to false.
};
// 将message组件,渲染到container中
render(vm, container);
// 将message组件放入instances中
// 在close函数中,会将vm从instance中删除掉
instances.push({ vm });
// 将container挂载到body上
document.body.appendChild(container.firstElementChild);
return {
// 返回对象中有close方法,使用者可以使用此close方法关闭显示
// 该close方法将visible置为false,进而引发transition组件的before-leave/after-leave钩子函数
close: () => ((vm.component.proxy as ComponentPublicInstance<{ visible: boolean }>).visible = false),
};
} as any;
// transition的before-leave钩子会调用onClose方法
// onClose方法通过闭包将id保存下来,调用close方法关闭Message,销毁组件
export function close(id: string, userOnClose?: (vm: MessageVM) => void): void {
const idx = instances.findIndex(({ vm }) => {
const { id: _id } = vm.component.props;
return id === _id;
});
if (idx === -1) {
return;
}
const { vm } = instances[idx];
if (!vm) return;
// 调用用户传入的onClose钩子函数
userOnClose?.(vm);
const removedHeight = vm.el.offsetHeight;
// 从instances中去掉当前vm
instances.splice(idx, 1);
// 调整其他Message实例的高度
const len = instances.length;
if (len < 1) return;
// 只需要调整index 大于当前Message的实例的高度
for (let i = idx; i < len; i++) {
const pos = parseInt(instances[i].vm.el.style["top"], 10) - removedHeight - 16;
instances[i].vm.component.props.offset = pos;
}
}
// 关闭所有Message
export function closeAll(): void {
// instances中逐个调用其close方法
for (let i = instances.length - 1; i >= 0; i--) {
const instance = instances[i].vm.component as any;
instance.ctx.close();
}
}
// 四种类型,都添加相应的方法
(["success", "warning", "info", "error"] as const).forEach((type) => {
Message[type] = (options) => {
if (typeof options === "string") {
options = {
message: options,
type,
};
} else {
options.type = type;
}
// 调用Message方法,生成Message
return Message(options);
};
});
Message.closeAll = closeAll;
export default Message;
复制代码
2.3 总结
- 使用instances变量维护所有的Message实例,方便控制各个实例的top offset;
- 在transition的before-leave/after-leave钩子函数中进行Message实例的销毁
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END