【Nova UI】十六、打造组件库之滚动条组件(中):探秘滑块的计算逻辑

491 阅读4分钟

序言

在上篇文章中,我们完成了滚动条组件开发的前期准备工作,包括理论推导、布局规划和基础设置。现在,我们将把这些准备转化为实际代码,开启滚动条组件的具体开发之旅🌟。我们会详细阐述如何实现各项功能,解决开发中的技术挑战,为用户带来更好的滚动体验🌐。

滚动条

在设计滚动条组件时,我们需要考虑横向和纵向两个方向的滚动条。这两个方向的滚动条在功能实现和表现形式上有诸多相似之处,但也存在一些细微的差异。为了提高代码的可维护性、可复用性和可扩展性,我们决定将滚动条单独提取出来,作为一个独立的组件进行开发。基于此,我们在 packages/components/scrollbar/src 目录下创建了 bar.ts 和 bar.vue 文件,它们将作为实现滚动条功能的核心文件,为后续的开发工作提供强有力的支撑 🏗️。

bar.ts

import { ExtractPropTypes } from 'vue'
import { useDirectionProp } from '@nova-ui/hooks'
import type bar from './bar.vue'

export const barProps = {
  direction: useDirectionProp(),
  always: Boolean,
} as const

export type BarType = ExtractPropTypes<typeof barProps>

export type BarInstance = InstanceType<typeof bar>

在 bar.ts 文件中,barProps 包含 direction 和 always 两个属性。direction 用于确定滚动条的方向,并且可以在其他组件中复用 🔄。always 这个属性用于控制滚动条是否始终显示,以此来满足不同的使用场景 🔧。通过 InstanceType<typeof bar> 导出的 BarInstance 类型,有助于在操作滚动条组件实例时确保类型的准确性,避免出现类型错误,而且会在 scrollbar 组件中使用 🌟。

bar.vue

<template>
  <div
    v-show="visible"
    ref="barRef"
    :class="[
      n.b(),
      n.m(direction),
      n.is('always', always)
    ]"
  >
    <div
      ref="thumbRef"
      :class="n.e('thumb')"
    >
    </div>
  </div>
</template>

<script lang="ts" setup>
const n = useNamespace('bar')
defineOptions({
  name: 'NBar',
})
const props = defineProps(barProps)
</script>

bar.vue 的基本 DOM 结构由两个 div 元素组成,一个作为轨道,另一个作为滑块。首先,我们会定义以下几个重要的变量:

  • size:用于存储滑块的长或宽,方便后续的样式调整 📏。
  • ratio:表示视图区域与可视区域的比例,这是计算滑块大小的关键数据 🔢。
  • visible:决定滚动条是否显示,会根据不同的情况进行调整 👁️。
  • distance:表示滑块滚动的距离,它会根据用户的操作而变化呢 📏。

滑块大小计算

我们需要获取视图区域和可视区域的 DOM 元素。在 scrollbar 组件中通过 provide 传递,在 bar 组件中通过 inject 获取,以下是以纵向滚动条为例的代码,完整代码可查看相应的仓库 🔍。

const scrollbar = inject(scrollbarInjectionKey)
const visible = ref(false)
const size = ref(0)
const ratio = ref(1)

const updateHandler = () => {
  const wrap = scrollbar?.wrapElement
  if (!wrap) return
  const { offsetHeight, scrollHeight }  = wrap
    
  const _ratio = offsetHeight / scrollHeight
  const height = _ratio * offsetHeight

  size.value = height
  ratio.value = _ratio
  visible.value = offsetHeight < scrollHeight
}

通过上述计算,我们可以得到所需的数值,进而为滑块添加合适的样式 🌟。

const thumbStyle = computed(() => {
  return {
    [props.direction === 'vertical' ? 'height' : 'width']: size.value ? addUnit(size.value) : undefined
  } 
})

滑块滚动距离

在完成滑块大小的计算和相关属性的获取之后,我们要计算滑块滚动的距离 🔢。

const scrollHandler = () => {
  const wrap = scrollbar?.wrapElement
  if (!wrap) return
  distance.value = wrap.scrollTop * ratio.value
}

这个计算很简单,就是将视图区域滚动的距离与比例相乘。根据这个结果,我们将修改滑块的样式:

const thumbStyle = computed(() => {
  return {
    [props.direction === 'vertical' ? 'height' : 'width']: size.value ? addUnit(size.value) : undefined,
    transform: `translate${ props.direction === 'vertical' ? 'Y' : 'X' }(${ distance.value }px)`
  }
})

至此,基本功能已成型 👏。接下来,我们来看看如何触发这些方法。

scrollbar 更新 bar的 滑块

在 VNode 更新之后,我们需要调用相应的方法,这里使用 onUpdated 生命周期来完成。为了更好地兼容不同的场景,当视图区域大小发生变化时,我们会使用 useResizeObserver 来监听 DOM 变化并调用 updateHandler 。

const update = () => {
    barUpdateHandler()
    barScrollHandler()
}

let stopResizeObserver: (() => void) | undefined = undefined
watch(() => props.noresize, (noresize) => {
    if (noresize) {
      stopResizeObserver?.()
    } else {
      const { stop } = useResizeObserver(wrapRef as unknown as MaybeComputedElementRef, update)
      stopResizeObserver = stop
    }
}, { immediate: true })

onUpdated(() => {
    update()
})

scrollbar 更新 bar的 滚动距离

在设置好滑块的大小之后,我们需要处理滚动距离。我们会对滚动区域进行监听,触发 bar 的相关函数。

const barScrollHandler = () => {
    barVerticalRef.value?.scroll()
    barHorizontalRef.value?.scroll()
}
const onScroll = () => {
    barScrollHandler()
    emits('scroll', {
      scrollTop: wrapRef.value?.scrollTop || 0,
      scrollLeft: wrapRef.value?.scrollLeft || 0,
    })
}

🦀🦀感谢看官看到这里,如果觉得文章不错的话🙌,点个关注不迷路⭐。
诚邀您加入我的微信技术交流群🎉,群里都是志同道合的开发者👨‍💻,大家能一起交流分享摸鱼🐟。期待与您在群里相见🚀,咱们携手在开发路上共同进步✨ ! 👉点我

感谢各位大侠一路相伴,实在感激! 不瞒您说,在下还有几个开源项目 📦,它们就像精心培育的幼苗 🌱,急需您的浇灌。要是您瞧着还不错,麻烦动动手指,给它们点亮几颗 Star ⭐,您的支持就是它们成长的最大动力,在此谢过各位大侠啦!