一、前言
在日常的前端项目开发中,我们时常需要使用到“消息提示”(以下简称“消息”)这个组件来帮助我们更好的给予用户提示,例如常见的“登录成功”、“操作成功”、“服务器异常”等等提示。
尽管市面上已经有一些组件库提供了这样的组件,例如国产的Element-Plus
(如下图)。但他们还是有一个缺点,即如果我仅仅需要消息提示这一组件,那么引入一个大型的组件库是完全多余的,对整个项目来说,也会使其依赖体积过分庞大。
此外,还需要考虑到用户自定义的需求。例如,我可能并不希望我的“消息”组件和别人的千篇一律,那么学习如何定制消息组件显然是很有必要的。
那么,如何使用Vue3.x
版本的组件式开发风格配合TypeScript
来实现这样的功能呢?以下,我将做一个简单的演示,需要注意依赖的版本问题。
二、分析消息组件的需求
在正式的实现一个消息组件之前,我们先要思考这个组件需要哪些功能,这部分我整理如下:
- 在页面顶部中心位置弹出消息通知框,并且能够设置停留在页面上的时间。
- 消息通知框有不同的类型,例如警告、错误、、通知、成功。
- 当多个消息通知框同时展示时,新出现的消息通知框应该在原有的下方展示,并且当原有的消息通知消失时,能够自动更新位置。
三、消息组件的实现
首先,创建一个Vue3.x
的项目。
(一)新建一个Message组件
以下是Message组件代码的一个示例,它支持自定义消息内容,并且能根据props
中传入消息类型的不同而展示不同的图标和样式,同时引入了一个名为lucide
的UI库,这个库相对轻量级,因此不必担心其占用问题。
唯一需要注意的是,下方的全局样式部分。它定义了两个动画帧,同时定义了一个名为message-fade-out
的类,这部分主要是用于给消息组件做入场和出场动画的。
它不能被放入组件私有样式里,因为我们的消息组件后续会作为一个Vue APP
挂载到一个HTML Element
上,这时由于该元素属于消息组件的父级节点,组件内部样式会对其不起作用。
<template><div class="message" :class="type"><span class="icon" v-if="showIcon"><component :is="iconComponent" /></span><span class="content">{{ content }}</span></div>
</template><script setup lang="ts">
import { computed } from 'vue';
import { Info, CheckCircle, AlertCircle, XCircle } from 'lucide-vue-next'; // 使用 lucide-vue-next 图标库interface Props {content: string;type?: 'info' | 'success' | 'warning' | 'error';showIcon?: boolean;
}const props = withDefaults(defineProps<Props>(), {type: 'info',showIcon: true,
});// 根据类型选择图标
const iconComponent = computed(() => {switch (props.type) {case 'success':return CheckCircle;case 'warning':return AlertCircle;case 'error':return XCircle;default:return Info;}
});
</script><style scoped>
/* 组件的私有样式 */
.message {position: relative;left: 50%;transform: translateX(-50%);padding: 12px 20px;border-radius: 8px;color: white;z-index: 1000;display: flex;align-items: center;gap: 10px;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);opacity: 0;animation: fadeIn 0.3s ease forwards;
}.message.info {
background-color: #3498db;
}.message.success {
background-color: #2ecc71;
}.message.warning {
background-color: #f1c40f;
}.message.error {
background-color: #e74c3c;
}.icon {
display: flex;
align-items: center;
}.content {
flex: 1;
word-break: break-word;
}</style><style>
.message-fade-out {animation: fadeOut 0.3s ease forwards !important;
}@keyframes fadeIn {from { opacity: 0; transform: translateX(-50%) translateY(-20px); }to { opacity: 1; transform: translateX(-50%) translateY(0); }
}@keyframes fadeOut {from { opacity: 1; transform: translateX(-50%) translateY(0); }to { opacity: 0; transform: translateX(-50%) translateY(-20px); }
}
</style>
(二)新建一个message.ts
的文件
其主要用于实现消息组件的逻辑,同时对外提供消息方法。主要实现思路如下:
1.首先定义一个消息对象和一个消息队列,消息对象由id
、app
对象和html
元素构成。之所以这么做,是因为我们的消息组件需要作为Vue App
挂载到一个具体的元素上,他们具备一一对应的关系。这样维护一个消息队列,可以更方便地计算多个消息展示时具体的高度。
2.然后编写展示消息的逻辑:
(1)对于初次使用消息组件的情况,先新建一个外层容器div,这么做可以更好的控制消息组件的样式。
(2)创建一个消息div,添加到上述容器的子节点中,接着消息组件作为APP
挂载到该元素上。
(3)更新消息位置,并设置一个定时器,使其到达指定时间后触发隐藏消息的方法。
3.消息隐藏方法的逻辑较为简单,核心为给消息挂载的el
元素上添加渐隐的class,使其触发动画效果,并通过侦听器,让其在动画结束后从DOM
中移除。
4.更新消息高度的方法,这部分核心是动态地根据队列中消息的index
来给其分配合适的高度。
5.最后,将四种类型的消息方法导出。
import { createApp } from 'vue';
import type { App } from 'vue';
import Message from '@/components/Message.vue';type MessageType = 'info' | 'success' | 'warning' | 'error';interface MessageInstance {id: number;app: App<Element>;el: HTMLElement;
}const messageQueue: MessageInstance[] = [];
let messageContainer: HTMLElement | null = null;
let messageId = 0;// 动态计算消息位置
function updateMessagePositions() {let currentTop = 20; // 初始顶部距离messageQueue.forEach((msg) => {const el = msg.el;const height = el.offsetHeight; // 获取实际高度el.style.top = `${currentTop}px`;currentTop += height + 10; // 累加高度和间隙});
}function showMessage(content: string, type: MessageType = 'info', duration: number = 3000) {if (!messageContainer) {messageContainer = document.createElement('div');messageContainer.style.position = 'fixed';messageContainer.style.top = '0';messageContainer.style.left = '0';messageContainer.style.width = '100%';messageContainer.style.pointerEvents = 'none'; // 防止拦截点击事件document.body.appendChild(messageContainer);}const id = messageId++;const el = document.createElement('div');el.style.position = 'absolute';el.style.left = '50%';el.style.transform = 'translateX(-50%)';el.style.transition = 'top 0.8s ease';//添加过渡,这样当一个消息消失时,其他消息的高度变化会有过渡效果。const messageApp = createApp(Message, { content, type });const messageInstance: MessageInstance = { id, app: messageApp, el };messageQueue.push(messageInstance);messageContainer.appendChild(el);messageApp.mount(el);// 等待 DOM 更新后计算位置updateMessagePositions()// 自动隐藏setTimeout(() => hideMessage(id), duration);
}function hideMessage(id: number) {const index = messageQueue.findIndex((msg) => msg.id === id);if (index === -1) return;const [messageInstance] = messageQueue.splice(index, 1);const el = messageInstance.el;// 添加淡出动画类el.classList.add('message-fade-out');// 动画结束后移除元素const onAnimationEnd = () => {el.removeEventListener('animationend', onAnimationEnd);messageInstance.app.unmount();el.remove();updateMessagePositions();};el.addEventListener('animationend', onAnimationEnd, { once: true });}export default {info(content: string, duration?: number) {showMessage(content, 'info', duration);},success(content: string, duration?: number) {showMessage(content, 'success', duration);},warning(content: string, duration?: number) {showMessage(content, 'warning', duration);},error(content: string, duration?: number) {showMessage(content, 'error', duration);},
};
(三)测试消息组件
我们可以任意新建一个新的组件,并通过按钮触发消息通知,例如:
const handleClick = () => {message.info('这是一个比较长的句子字字字字字……', 5000)
}
页面上展示效果如下:
四、总结
恭喜你!顺利看到这里,想必已经掌握了如何自主实现一个消息组件。在这个过程中,相信你对DOM
操作的理解也进一步加深了,接下来可以任意定制想要的内容啦!