前言
首先我们分析一下已经原生的 ElMessage 具有哪些行为和状态,如下图:
- 使用上:通过一个方法传入一个配置对象就可以显示组件
- 行为上:消息组件拥有出场动画和退场动画,支持多消息堆叠显示,可以自定义类型、消息内容、存在时间等等
它具体如何实现?从简到繁,我们先忽略多条 message 堆叠显示的情况实现一个最简版的ElMessage,掌握核心实现后再完善功能。如下图:

再来实现完整版:

关键概念
先对齐一下关键概念
createApp:在 vue 中我们通过 createApp 方法创建一个应用实例,该方法需要传入一个组件作为应用的根组件,另外还可以通过 createApp 的第二个参数给这个根组件传递 props,返回的实例可以通过内部的 mount 方法将根组件挂载到某个元素内部
命令式组件 :命令式组件是一种通过直接调用方法或操作来实现功能的组件,它允许开发者以命令的方式控制组件的行为,而不是通过声明式的数据绑定。它的存在意义在于提供更灵活的交互方式,适用于需要动态、复杂交互逻辑的场景,例如模态框、通知提示、拖拽组件等,这些场景中组件的行为往往需要根据用户的操作动态调整,而命令式的方式能够更直接地实现这种需求。
最简实现
目标:
- 封装myElMessage函数,传递一个配置对象,调用该函数可以在页面渲染一个Message组件,并在指定时间后组件退场并被卸载
全局 css
css
.my-el-message {
position: fixed;
top: 20px;
z-index: 9999;
left: 50%;
transform: translateX(-50%);
transition: all 0.3s ease-in-out;
}
myElMessage.js
javascript
// 引入要挂载的组件
import Message from "@/components/Message.vue";
// 将组件挂载到html上的方法
import { createApp } from "vue";
export function myElMessage(options, cb) {
//参数归一化,传入的options除了是对象还可能是字符串
const defaultOptions = {
message: "",
duration: 3000,
};
if (typeof options === "object") {
Object.keys(options).forEach((key) => {
defaultOptions[key] = options[key] || defaultOptions[key];
});
} else if (options) {
defaultOptions.message = options;
}
//创建应用实例
const app = createApp(Message, {
...defaultOptions,
});
//创建挂载元素
const div = document.createElement("div");
//设置类名统一样式
div.className = "my-el-message";
//通过provide将方法暴露给组件
app.provide("close", () => {
app.unmount(div);
document.body.removeChild(div);
});
//将message组件挂载到节点上
app.mount(div);
//将节点添加到body中
document.body.appendChild(div);
}
Message.vue
xml
<template>
<transition @after-leave="onAfterLeave" name="message">
<div v-if="showMessage" class="message">
{{ message }}
</div>
</transition>
</template>
<script setup>
import { ref, onMounted, inject } from "vue";
//注入卸载方法
const close = inject("close");
//通过传入的props配置message的功能
const props = defineProps({
message: {
type: [String, Object],
default: "This is a message",
},
duration: {
type: Number,
default: 3000,
},
});
//用来触发transition过渡动画
const showMessage = ref(false);
//组件挂载后触发transition过渡动画,一段时间后触发退场动画
onMounted(() => {
showMessage.value = true;
setTimeout(() => {
showMessage.value = false;
}, props.duration);
});
//消息组件退场动画结束后卸载应用
const onAfterLeave = () => {
close();
};
</script>
<style>
.message {
background-color: #f3f3f3;
border: 1px solid #dcdcdc;
padding: 6px 13px;
color: #969696;
width: fit-content;
border-radius: 5px;
}
.message-enter-active,
.message-leave-active {
transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
}
.message-enter-from,
.message-leave-to {
opacity: 0;
transform: translateY(-100%);
}
</style>
App.vue使用
xml
<template>
<el-button type="" @click="triggerMessage">我的ElMessage消息</el-button>
</template>
<script setup>
import { myElMessage } from "@/utils/myElMessage";
const triggerMessage = () => {
//通过调用方法传入配置对象触发组件渲染
myElMessage({
message: "hello.",
duration: 3000,
});
};
</script>
要点解析
- myElMessage.js
- 首先对传入的参数进行归一化
- 其次使用 createApp 方法传入 Message 组件对象作为根组件,并传递配置对象作为Message 组件的 props,获取返回的应用实例 app。
- 创建一个新的 div 节点,作为被挂载节点,存放于 body 中。
- 而后我们通过应用实例内部的 provide 给 Message 组件传递卸载方法,便于在 Message 组件内部控制卸载时机
- 最后将Message挂载到创建好的节点上,并将节点添加到 body 中
- Message.vue
- inject 注入卸载方法。
- 组件初始挂载完成后切换 showMessage 触发过渡动画。
- 过渡动画结束后执行卸载逻辑,将 div 容器节点和组件都进行卸载。
实现效果

完整堆叠显示实现
目标:
- 在上面的基础上,能够使渲染的多个 message 组件按顺序排列,并且某个 message 组件退场时后面的组件会跟着补上。
全局 css
css
.my-el-message {
position: fixed;
top: 20px;
z-index: 9999;
left: 50%;
transform: translateX(-50%);
transition: all 0.3s ease-in-out;
}
myElMessage.js
javascript
// 引入要挂载的组件
import Message from "@/components/Message.vue";
// 将组件挂载到html上的方法
import { createApp } from "vue";
//新增代码,创建一个代理数组,存放页面message节点排列顺序,并在数组内容改变时重新分配节点top值
const messageItemListProxy = new Proxy([], {
set(target, prop, value, receiver) {
const res = Reflect.set(...arguments);
//每当消息组件数组更新时刷新一遍top值
for (let i = 0; i < receiver.length; i++) {
receiver[i].style.top = `${i * 50 + 20}px`;
}
return res;
},
});
export function myElMessage(options, cb) {
//参数归一化
const defaultOptions = {
message: "",
duration: 3000,
};
if (typeof options === "object") {
Object.keys(options).forEach((key) => {
defaultOptions[key] = options[key] || defaultOptions[key];
});
} else if (options) {
defaultOptions.message = options;
}
const app = createApp(Message, {
...defaultOptions,
});
const div = document.createElement("div");
//新增代码,将创建的元素添加到数组中
messageItemListProxy.push(div);
//设置类名统一样式
div.className = "my-el-message";
//通过provide将方法暴露给组件
//新增代码,传递删除列表中的message节点方法
app.provide("rmList", () => {
const index = messageItemListProxy.findIndex((item) => item === div);
messageItemListProxy.splice(index, 1);
});
app.provide("close", () => {
app.unmount(div);
document.body.removeChild(div);
});
//将组件挂载到html上
app.mount(div);
//将div添加到body中
document.body.appendChild(div);
}
Message.vue
xml
<template>
<transition @after-leave="onAfterLeave" name="message">
<div v-if="showMessage" class="message">
{{ message }}
</div>
</transition>
</template>
<script setup>
import { ref, onMounted, inject } from "vue";
//注入方法
const close = inject("close");
//新增代码,注入删除message列表元素方法
const rmList = inject("rmList");
const props = defineProps({
message: {
type: [String, Object],
default: "This is a message",
},
duration: {
type: Number,
default: 3000,
},
});
const showMessage = ref(false);
onMounted(() => {
showMessage.value = true;
setTimeout(() => {
showMessage.value = false;
//新增代码,在动画触发时删除当前列表中的message节点
rmList();
}, props.duration);
});
//消息组件退场动画结束后卸载应用
const onAfterLeave = () => {
close();
};
</script>
<style>
.message {
background-color: #f3f3f3;
border: 1px solid #dcdcdc;
padding: 6px 13px;
color: #969696;
width: fit-content;
border-radius: 5px;
}
.message-enter-active,
.message-leave-active {
transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
}
.message-enter-from,
.message-leave-to {
opacity: 0;
transform: translateY(-100%);
}
</style>
要点解析
- myElMessage.js
- 新增一个代理数组,用来存放添加到页面的 message 容器节点 div,在数组内容发生改变时遍历数组重新为每个 div 节点赋新的 top 值控制位置。
- 在创建容器节点 div 后将节点推入代理数组中,并为该节点根据 index 赋上 top 值
- 新增 rmList 方法,用于删除代理数组中的当前容器节点,触发 top 的变化。
- Message.vue
- 新增 inject 注入 rmList 方法。
- 在触发过渡动画时调用 rmList 方法,刷新其他 message 节点的 top 值,使动画更流畅。
最终效果

后续可以通过自定义props的内容来扩展组件的功能,比如增加 type 控制消息类型等等。