前端组件库造轮子------Message组件开发教程
前言
本系列旨在记录前端组件库开发经验,我们的组件库项目目前已在Github
开源,下面是项目的部分组件。文章会详细介绍一些造组件库轮子的技巧并且最后会给出完整的演示demo。
文章旨在总结经验,开源分享,有问题的话也希望路过的大佬指正。
组件开发流程
样式和动画
首先我们来考虑样式,对于message
的调用我们下面再讲。
样式的话,无非就是实现一个这样消息弹出框,同时加上弹出时的动画,这里借助vue
的transition
来实现。
这些都是简单的内容,代码量很少我就直接贴在这里了。
ts
<transition name="message-fade">
<div class="message">
<p class="message-content"></p>
</div>
</transition>
.message {
position: fixed;
top: 20px;
left: 50%;
z-index: 50;
box-sizing: border-box;
display: flex;
align-items: center;
padding: 15px 15px 15px 20px;
overflow: hidden;
background-color: #f0f9eb;
border: 1px solid #ebeef5;
border-radius: 5px;
transition: opacity 0.3s, transform 0.4s, top 0.4s;
transform: translateX(-50%);
}
.message-fade-enter-active,
.message-fade-leave-active {
opacity: 0;
transform: translate(-50%, -100%);
}
.message-content {
color: #67c23a;
font-size: 14px;
margin: 0;
}
当然现在这个message
是只有空壳一样的内容,我们需要实现的效果是,有一个message
函数接口,我们可以调用这个接口后弹出这个消息框。这其实就是和其它一些组件不太一样的地方,他的实现更倾向于一个接口函数一样。
写一个函数当然不是问题,问题是怎么实现调用函数后渲染我们的消息框出来。
这里就需要了解一下vue
中渲染DOM
的知识点了。
h函数和render函数
在vue
中,很多文件的开发都是在.vue
文件的,这种文件开发是分为三大块来写,可以像类似写HTML
时的感觉,这也是vue
的卖点之一,让新手更易于上手。
但是我们要知道,.vue
实际上也是需要通过一些打包工具来编译成js
代码才能执行。
h函数
就是把.vue
中的代码编辑成一个虚拟DOM
,最终会把template
解析为render
函数返回虚拟DOM
,这点可以在Vue Dev Tools
中看到:
也就是说,h函数
是负责创建虚拟DOM
,render
是负责把这个虚拟DOM
返回出去
接口函数
通过上面的介绍,大概不难猜出这个接口函数应该如何实现了,其实就是创建一个虚拟DOM
出来包裹住我们的message
组件,在利用render
函数渲染出来即可。
ts
import element from "./message.vue";
import { createVNode, render } from "vue";
export default function message(options) {
if (typeof options === "string") {
options = {
message: options as string,
};
}
const params = {
...options,
};
// vue2 的写法
// new Vue(render:() => createVNode(element)).mount();
const div = document.createElement("div"); // 创建一个div
const vnode = createVNode(element, params); // 创建一个message组件的虚拟DOM
render(vnode, div); // 渲染虚拟DOM
document.body.appendChild(div.firstElementChild); // 加入到body中
}
那我们既然可以调用message
接口函数了,那么在message
组件中还有一些逻辑需要实现------在执行结束后关闭掉弹出来的消息框。
这里就是利用v-show
控制开关消息框,用定时器回调来解决关闭,我们可以利用props
接收存在时间duration
,这样我们的基本功能就算完成了,但是message
组件还存在很多细节可以补充。
ts
// message.vue
<transition name="message-fade">
<div class="message" v-show="visible">
<p class="message-content">{{ message }}</p>
</div>
</transition>
const visible = ref(false);
let timer = null;
const start = () => {
visible.value = true;
if (timer !== null) {
clearTimeout(timer);
}
if (props.duration > 0) {
timer = setTimeout(() => {
visible.value = false;
}, props.duration);
}
};
onMounted(() => {
start();
});
onUnmounted(() => {
if (timer !== null) {
clearTimeout(timer);
}
});
回调删除节点的性能优化
在刚刚上面完成的组件中,会发现当我们多次触发了message
后,哪怕duration
过了,节点也依然存在在body
中,这些节点只是被隐藏了,并没有随着持续时间结束后删除掉。
这样显然是不太合理的,并且操作多了会存在一些性能问题,因此我们需要在这个组件在持续时间结束后可以被删除掉。
这里我们可以在动画结束后,派发出一个destroy
事件
ts
<transition name="message-fade" @after-leave="$emit('destroy')"> // 派发删除操作
<div class="message" v-show="visible">
<p class="message-content">{{ message }}</p>
</div>
</transition>
这个$emit('destroy')
会调用我们传进来的props
中的onDestroy
函数
ts
// message.ts
const div = document.createElement("div");
const vnode = createVNode(element, params);
+ vnode.props.onDestroy = () => { // 在参数props中挂载销毁函数
+ render(null, div); // 利用render移除div节点
+ };
render(vnode, div);
document.body.appendChild(div.firstElementChild);
连续多次弹出的用户体验优化
当我们连续触发多次message
时,会弹出多个消息,对于这多个消息,我们不希望会重叠在一起发生覆盖的情况,我们希望的是可以像下面这样。
那我们如何实现上面的效果呢?
我们可以在props
中加一个offset
属性,该属性为message
组件的离视屏顶部的距离。
然后我们还需要知道上个节点的距离是多少,因此我们需要把连续点出的节点都记录起来,具体来说就是用一个数组把他们存起来,数组中的值按上一节点的offset
基础上加合适的距离即可。
ts
// message.ts
+ const instances: VNode[] = [];
export default function message(options) {
...
+ let offset = options.offset || 20;
+ instances.forEach((vnode: VNode) => {
+ offset += vnode.el.offsetHeight + 20;
+ });
const params = {
...options,
+ offset,
};
const div = document.createElement("div");
const vnode = createVNode(element, params);
vnode.props.onDestroy = () => {
render(null, div); // render会移除dom,注意:此方法在vue2中无法使用
+ instances.pop();
};
render(vnode, div);
document.body.appendChild(div.firstElementChild);
+ instances.push(vnode);
}
同时,我们需要在渲染出message
组件中,加上新的offset
位置。
typescript
// message.vue
<transition name="message-fade" @after-leave="$emit('destroy')">
<div class="message" v-show="visible" :style="topStyle"> // 更改top位置
<p class="message-content">{{ message }}</p>
</div>
</transition>
const topStyle = computed(() => {
return {
top: `${props.offset}px`,
};
});
演示demo
结语
Message
组件的核心开发功能就是上面这些,其他更多的详细功能开发可以参考Hview-ui
项目源码
如果想要了解更多的组件轮子开发,或者组件库开发流程,更多详细的组件开发过程更新在GitHub
项目源码,最后觉得我们项目or文章不错可以点个star,点点小手支持一下,也欢迎各路大佬为我们的开源项目添砖加瓦。