一、前言
在日常的前端项目开发中,我们时常需要使用到“消息提示”(以下简称“消息”)这个组件来帮助我们更好的给予用户提示,例如常见的“登录成功”、“操作成功”、“服务器异常”等等提示。
尽管市面上已经有一些组件库提供了这样的组件,例如国产的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
操作的理解也进一步加深了,接下来可以任意定制想要的内容啦!