如何封装一个符合组件封装原则的通用物料组件?

302 阅读3分钟

如何封装一个符合组件封装原则的通用物料组件?

以下是一些组件封装的关键原则和封装思想

单一职责原则、高内聚低耦合原则、可扩展性与灵活性、响应式设计、代码规范与可维护性、插槽等等等等

本文将从一个前端常见的 Popover 气泡组件出发,做个最佳实践的讲解。

先来看看组件实现后应达到的效果

WeChat_20241207113519_20241207_113827.gif

单一职责!很显然,一个气泡组件具备两个能力,一个是触发气泡弹出的视图(如图中头像),另一个就是弹出图层中展示的内容。明确这两个基本能力后,我们可以先来实现一个基础板子。
<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'

// 控制 menu 展示
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 ,这里提供一个示例方法。
/**
 * 初始化 rem 基准值,最大为 40px
 */
export const useREM = () => {
  // 定义最大的 fontSize
  const MAX_FONT_SIZE = 40

  // 监听 html 文档被解析完成的事件
  document.addEventListener('DOMContentLoaded', () => {
    // 获取 html 标签
    const html = document.querySelector('html')
    // 获取根元素 fontSize 标准,屏幕宽度 / 10。(以 Iphone 为例 Iphone 6 屏幕宽度为 375,则标准 fontSize 为 37.5)
    let fontSize = window.innerWidth / 10
    // 获取到的 fontSize 不允许超过我们定义的最大值
    fontSize = fontSize > MAX_FONT_SIZE ? MAX_FONT_SIZE : fontSize
    // 定义根元素(html)fontSize 的大小 (rem)
    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'

// 定义指定位置的 Enum
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
    }
  })
})

至此,我们实现了一个基本的通用气泡组件。对于组件适配移动端或者多浏览器,或者更好的组件封装思路,大家有什么好的想法、建议或者扩展欢迎在评论区留言。

1.webp