发财UI Message组件的实现

[发财UI] Message组件的实现

前言

最近在实现Message组件,就是会从屏幕顶端弹出的一个小提醒,过一会儿就消失了。我个人非常喜欢这个设计,感觉在后续的复用性也很高,于是就打算自己手写一个作为发财UI的组件

支持的功能

目前的Message有四种类型:

  1. 普通提醒normal

  2. 成功提醒success

  3. 警告提醒warning

  4. 错误提醒error

同时还支持设置持续的时间:

使用方法

是不是非常简单😉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<button @click="popNormalMsg">打开一个普通提醒</button>
</div>
</template>

<script lang="ts">
import {popMessage} from "../../lib/popMessage";

export default {
name: "Message1.demo",
components: {Button},
setup() {
const popNormalMsg = () => {
popMessage({
message: '这是一个全局显示的普通提醒', //提醒内容
msgType: 'normal', //提醒类型normal success error warning
closeDelay: '2000', //显示的时长,以ms为单位
})
}
return {popNormalMsg}
}
}
</script>

实现过程

如何实现不同类型的切换?

其实切换类型只是切换图标而已哈哈哈

这里使用了IconPark图标库,这里使用了一个投机取巧的办法,把不同的图标命名为相应的type,可以节省一些处理的步骤

href=#warning msgType='warning'
href=#error msgType='error'
1
const typeIndicator = `<use href="#${props.msgType}"></use>`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div ref="msgDiv" class="rich-message">
<svg class="iconpark-icon" v-html="typeIndicator">
//2️⃣typeIndicator的内容会原封不动的跑到这里
</svg>
//3️⃣最后和svg标签一起变成type对应的图标
<div class="rich-message-msgText">{{ message }}</div>
</div>
</template>

<script lang="ts">

export default {
name: "Message",
props: {
message: {
type: String,
required: true,
},
msgType: {
type: String,
default: 'normal',
},
},
setup(props) {
const typeIndicator = `<use href="#${props.msgType}"></use>`
return {typeIndicator}
//1️⃣接受到传来的type,然后typeIndicator会自动变为相应的图标use标签
}
}
</script>

如何实现Message的弹出和消失?

使用了CSS的transform,实际上就是Message在初始状态下是藏在画面外的,通过添加一个.message-active的类来让它显示出来,在经过closeDelay毫秒后移除.message-active类。

1
2
3
4
5
6
7
8
9
10
.rich-message {
...
transform: translateY(-100px);
transition: all 250ms;

&.message-active {
transform: translateY(0px);
opacity: 1;
}
}

如何实现往下排列而非堆叠?

为了让他们能够一个一个的排列下来而不是堆叠在一起,我想到了insertAdjacentElement()方法

1
element.insertAdjacentElement(position, element);

position有下面四种取值

  • 'beforebegin': 在该元素本身的前面。

  • 'afterbegin':只在该元素当中,在该元素第一个子孩子前面。

  • 'beforeend':只在该元素当中,在该元素最后一个子孩子后面。

  • 'afterend': 在该元素本身的后面。

不难发现这里似乎可以使用beforeendafterend。经过我的思考,为了保持DOM树的整洁,我采用了创建一个msgContainer的div来存放所有的Message的方法,因此我也相应的使用了beforeend

1
2
3
4
5
6
7
8
let msgContainer = document.getElementById('msgDiv')
if (msgContainer === null) {
msgContainer = document.createElement('div')
msgContainer.id = 'msgDiv'
document.body.appendChild(msgContainer)
}
const div = document.createElement('div'); //这个div就是Message所在的div
msgContainer.insertAdjacentElement('beforeend', div)

msgContainer一个CSS样式

1
2
3
4
5
6
7
8
9
10
11
#msgDiv {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);

display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

如何实现添加和移除.message-active类?

如果msgDiv在创建时就带有.message-active类,那么将会闪现在页面中,所以msgDiv应该是在渲染后被添加了.message-active类,为了实现这个效果,使用了一个setTimeout()

同时在closeDelay之后将这个类移除。

但是这样存在一个问题,这个msgDiv只是看不见了,依然存在于DOM树中。

1
2
3
4
5
6
7
8
setTimeout(() => {
msgDiv.classList.add('message-active')
}, 0)

setTimeout(() => {
msgDiv.classList.remove('message-active')
}, closeDelay * 1);

如何将隐藏的Message从DOM树中移除

在Message的淡出动画结束后移除就好了,这里使用了.ontransitionendAPI,但是还存在问题,即如果有多个Message,他们会同时消失,原因是虽然每个Message在创建时都会有一个计时器,但是在移除时却是所有的msgDiv一起移除,因此需要有区分的方法。

1
2
3
4
5
6
7
setTimeout(() => {
msgDiv.classList.remove('message-active')
msgDiv.ontransitionend = () => {
app.unmount();
div.remove();
}
}, closeDelay * 1);

如何区分不同的Message?

在本项目中,我使用了随机生成ID的方式,如此一番就能精准的控制每个msgDiv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function randomLetter(len) {
let str = '';
for (let i = 0; i < len; i++) {
str += String.fromCharCode(~~(Math.random() * 26 + 65));
}
return str;
}

const msgId = randomLetter(~~(Math.random() * 10 + 30)) //生成了一个随机字符串
const app = createApp({
render() {
return h(Message, {
message,
msgType,
id: msgId,
});
}
});
app.mount(div);

const msgDiv = document.getElementById(String(msgId))

setTimeout(() => {
msgDiv.classList.add('message-active')
}, 0)

setTimeout(() => {
msgDiv.classList.remove('message-active')
msgDiv.ontransitionend = () => {
app.unmount();
div.remove();
}
}, closeDelay * 1);

最后的一个小细节

我们使用了一个msgContainer将所有的Message包裹的起来,从而实现顺序排列,但是在最后一个Message消失后,msgContainer会作为一个空的div仍然存在于DOM树中,这很不环保,因此在最后一个Message消失后将msgContainer也一并移除

1
2
3
4
5
6
7
8
9
10
setTimeout(() => {
msgDiv.classList.remove('message-active')
msgDiv.ontransitionend = () => {
app.unmount();
div.remove();
if (msgContainer.children.length === 0) {
msgContainer.remove()
}
}
}, closeDelay * 1);

后记

以上就是我写Message组件时的所有思路,如果你有更好的想法,欢迎留言告诉我呀~


发财UI Message组件的实现
https://bald3r.wang/2022/07/30/33-发财UI-Message组件的实现/
作者
Allen
发布于
2022年7月30日
许可协议