从零开始实现命令式组件ElMessage(附代码)

前言

首先我们分析一下已经原生的 ElMessage 具有哪些行为和状态,如下图:

  • 使用上:通过一个方法传入一个配置对象就可以显示组件
  • 行为上:消息组件拥有出场动画和退场动画,支持多消息堆叠显示,可以自定义类型、消息内容、存在时间等等

它具体如何实现?从简到繁,我们先忽略多条 message 堆叠显示的情况实现一个最简版的ElMessage,掌握核心实现后再完善功能。如下图:

再来实现完整版:

关键概念

先对齐一下关键概念

createApp:在 vue 中我们通过 createApp 方法创建一个应用实例,该方法需要传入一个组件作为应用的根组件,另外还可以通过 createApp 的第二个参数给这个根组件传递 props,返回的实例可以通过内部的 mount 方法将根组件挂载到某个元素内部

命令式组件命令式组件是一种通过直接调用方法或操作来实现功能的组件,它允许开发者以命令的方式控制组件的行为,而不是通过声明式的数据绑定。它的存在意义在于提供更灵活的交互方式,适用于需要动态、复杂交互逻辑的场景,例如模态框、通知提示、拖拽组件等,这些场景中组件的行为往往需要根据用户的操作动态调整,而命令式的方式能够更直接地实现这种需求。

最简实现

目标:

  1. 封装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
  1. 首先对传入的参数进行归一化
  2. 其次使用 createApp 方法传入 Message 组件对象作为根组件,并传递配置对象作为Message 组件的 props,获取返回的应用实例 app。
  3. 创建一个新的 div 节点,作为被挂载节点,存放于 body 中。
  4. 而后我们通过应用实例内部的 provide 给 Message 组件传递卸载方法,便于在 Message 组件内部控制卸载时机
  5. 最后将Message挂载到创建好的节点上,并将节点添加到 body 中
  • Message.vue
  1. inject 注入卸载方法。
  2. 组件初始挂载完成后切换 showMessage 触发过渡动画。
  3. 过渡动画结束后执行卸载逻辑,将 div 容器节点和组件都进行卸载。

实现效果

完整堆叠显示实现

目标:

  1. 在上面的基础上,能够使渲染的多个 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
  1. 新增一个代理数组,用来存放添加到页面的 message 容器节点 div,在数组内容发生改变时遍历数组重新为每个 div 节点赋新的 top 值控制位置。
  2. 在创建容器节点 div 后将节点推入代理数组中,并为该节点根据 index 赋上 top 值
  3. 新增 rmList 方法,用于删除代理数组中的当前容器节点,触发 top 的变化。
  • Message.vue
  1. 新增 inject 注入 rmList 方法。
  2. 在触发过渡动画时调用 rmList 方法,刷新其他 message 节点的 top 值,使动画更流畅。

最终效果

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

相关推荐
孤月葬花魂5 分钟前
JavaScript 中的 Promise API 全面解析
前端·javascript
几道之旅6 分钟前
Electron 应用打包全指南
前端·javascript·electron
shushushu10 分钟前
Web如何自动播放音视频
前端·javascript
帅夫帅夫15 分钟前
前端存储入门:Cookie 与用户登录状态管理
前端·架构
陈随易19 分钟前
程序员的新玩具,MoonBit(月兔)编程语言科普
前端·后端·程序员
傻球22 分钟前
前端实现文本描边
前端·canvas
snakeshe101023 分钟前
1. 实现 useEffect
前端
前端进阶者25 分钟前
天地图InfoWindow插入React自定义组件
前端·javascript
Nu1134 分钟前
@babel/preset-env的corejs、@babel/plugin-transform-runtime的corejs之间区别
前端·babel
用户698135449106135 分钟前
three.js绘制中国地理数据
前端