如何封装一个符合组件封装原则的通用物料组件?
以下是一些组件封装的关键原则和封装思想
单一职责原则、高内聚低耦合原则、可扩展性与灵活性、响应式设计、代码规范与可维护性、插槽等等等等
本文将从一个前端常见的 Popover 气泡组件出发,做个最佳实践的讲解。
先来看看组件实现后应达到的效果

单一职责!很显然,一个气泡组件具备两个能力,一个是触发气泡弹出的视图(如图中头像),另一个就是弹出图层中展示的内容。明确这两个基本能力后,我们可以先来实现一个基础板子。
<template>
<div class="relative" @mouseleave="onMouseleave" @mouseenter="onMouseenter">
<div>
<slot name="reference" />
</div>
<transition name="slide">
<div
v-show="isVisable"
ref="contentTarget"
class="pop-content"
>
<slot />
</div>
</transition>
</div>
</template>
<script>
const DELAY_TIME = 100
</script>
<script setup>
import { ref } from 'vue'
const isVisable = ref(false)
let timeout = null
const onMouseenter = () => {
isVisable.value = true
if (timeout) {
clearTimeout(timeout)
}
}
const onMouseleave = () => {
timeout = setTimeout(() => {
isVisable.value = false
timeout = null
}, DELAY_TIME)
}
</script>
<style lang="scss" scoped>
// slide 展示动画
.slide-enter-active {
transition: opacity 0.3s, transform 0.3s;
}
.slide-leave-active {
transition: opacity 0.3s, transform 0.3s;
}
.slide-enter-from,
.slide-leave-to {
transform: translateY(20px);
opacity: 0;
}
.pop-content {
position: absolute;
padding: 0.25rem;
z-index: 20;
background-color: rgb(255 255 255);
border-width: 1px;
border-radius: 0.375rem;
}
</style>
这里我们分别设置了一个具名插槽和一个匿名插槽对应触发视图和弹出视图。在样式中这里使用了 rem 的响应式单位,具体是在浏览器完全加载和解析了HTML文档,并构建了DOM树之后,设置根元素 html 的 fong-size ,这里提供一个示例方法。
export const useREM = () => {
const MAX_FONT_SIZE = 40
document.addEventListener('DOMContentLoaded', () => {
const html = document.querySelector('html')
let fontSize = window.innerWidth / 10
fontSize = fontSize > MAX_FONT_SIZE ? MAX_FONT_SIZE : fontSize
html.style.fontSize = fontSize + 'px'
})
}
到这里我们已经实现了一个基础气泡组件,具体的气泡样式和触发的元素样式大家可以根据自己需求 DIY。
下面我们对该气泡组件进行一个扩展。控制气泡的弹出位置,这里只做一个基础的位置可控(左上,右上,左下,右下)
首先指定所有可选位置常量,并生成 enum。
<script>
const PROP_TOP_LEFT = 'top-left'
const PROP_TOP_RIGHT = 'top-right'
const PROP_BOTTOM_LEFT = 'bottom-left'
const PROP_BOTTOM_RIGHT = 'bottom-right'
const placementEnum = [
PROP_TOP_LEFT,
PROP_TOP_RIGHT,
PROP_BOTTOM_LEFT,
PROP_BOTTOM_RIGHT
]
</script>
然后创建一个接收弹出位置的 prop
const props = {
placement: {
type: String,
default: 'bottom-left',
validator(val) {
const result = placementEnum.includes(val)
if (!result) {
throw new Error(`你的 placement 必须是 ${placementEnum.join('、')} 中的一个`)
}
}
}
}
接着获取元素 DOM ,生成气泡样式对象,监听气泡展示变化来计算气泡样式。
<div ref="referenceTarget">
<!-- 具名插槽 -->
<slot name="reference" />
</div>
<!-- 气泡展示动画 -->
<transition name="slide">
<div
ref="contentTarget"
...
>
<slot />
</div>
</transition>
const contentStyle = ref({
top: 0,
left: 0
})
const referenceTarget = ref(null)
const contentTarget = ref(null)
const useElementSize = (target) => {
if (!target) return {}
return {
width: target.offsetWidth,
height: target.offsetHeight
}
}
watch(isVisable, (val) => {
if (!val) {
return
}
nextTick(() => {
switch (props.placement) {
case PROP_TOP_LEFT:
contentStyle.value.top = 0
contentStyle.value.left =
-useElementSize(contentTarget.value).width + 'px'
break
case PROP_TOP_RIGHT:
contentStyle.value.top = 0
contentStyle.value.left =
useElementSize(referenceTarget.value).width + 'px'
break
case PROP_BOTTOM_LEFT:
contentStyle.value.top =
useElementSize(referenceTarget.value).height + 'px'
contentStyle.value.left =
-useElementSize(contentTarget.value).width + 'px'
break
case PROP_BOTTOM_RIGHT:
contentStyle.value.top =
useElementSize(referenceTarget.value).height + 'px'
contentStyle.value.left =
useElementSize(referenceTarget.value).width + 'px'
break
}
})
})
至此,我们实现了一个基本的通用气泡组件。对于组件适配移动端或者多浏览器,或者更好的组件封装思路,大家有什么好的想法、建议或者扩展欢迎在评论区留言。
