什么,你还不会手撕vue3瀑布流组件

瀑布流布局是一种流行的布局方式,常见于图片展示、电商产品列表等场景。

它的核心思想是将元素按照一定规则分配到多列中,每列的高度由其中的元素决定,从而实现了一种类似自然堆砌的视觉效果。

相比传统的网格布局,瀑布流布局更加灵活、动态,能更好地利用空间。

这个瀑布流组件的实现采用了 Vue 3 的 Setup 语法,充分利用了 Composition API 的特性。它定义了一些重要的响应式数据,如 props 用于接收外部传入的列表数据、列宽、列间距等配置项,emits 用于触发自定义事件通知父组件,wrappercontent 分别用于获取容器元素和内容元素的引用。

具体的代码实现

xml 复制代码
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, withDefaults } from 'vue'

const props = withDefaults(
  defineProps<{
    list: any[]
    columnWidth?: number // 列宽
    columnGap?: number // 列间距
    hasMore: boolean
  }>(),
  {
    columnWidth: 172,
    columnGap: 20,
    hasMore: true,
  },
)

const emits = defineEmits(['loadMore'])
const wrapper = ref<HTMLElement | null>(null)
const content = ref<HTMLElement | null>(null)
const flowHeight: number[] = []

/// 绘制瀑布流
function flowDraw() {
  if (!content.value)
    return
  // 初始化列高
  const columnCount = getColumnCount()
  flowHeight.length = columnCount
  for (let i = 0; i < columnCount; i++) {
    flowHeight[i] = 0
  }
  // 设置容器宽(居中布局)
  const itemW = props.columnWidth + props.columnGap
  content.value.style.width = `${itemW * columnCount - props.columnGap}px`

  // 绘制 item 位置
  const doms = content.value.querySelectorAll('.WaterfallItem')
  doms.forEach((dom: any) => {
    const minIdx = getMinIndex(flowHeight)
    dom.style.left = `${minIdx * itemW}px`
    dom.style.top = `${flowHeight[minIdx]}px`
    flowHeight[minIdx] += dom.offsetHeight
  })
  // 设置容器高
  content.value.style.height = `${Math.max(...flowHeight)}px`
}

/// 获取列的数量
function getColumnCount(): number {
  if (!wrapper.value)
    return 0
  const itemW = props.columnWidth + props.columnGap
  const num = (wrapper.value.offsetWidth + props.columnGap) / itemW
  return Math.min(Math.floor(num), props.list.length)
}

/// 获取最小值的索引 index
function getMinIndex(list: number[]) {
  const min = Math.min(...list)
  return list.indexOf(min)
}

/// 监听窗口变化重绘瀑布流布局
const timer = ref()
function onResize() {
  if (timer.value) {
    clearTimeout(timer.value)
    timer.value = null
  }
  timer.value = setTimeout(() => {
    flowDraw()
  }, 300)
}
const loadMoreTrigger = ref()
const observer = ref<IntersectionObserver | null>(null)
function handleIntersect(entries) {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      loadMore()
    }
  })
}
function loadMore() {
  if (!props.hasMore)
    return
  console.log('1111', 1111)
  emits('loadMore')
  onResize()
}
onMounted(() => {
  flowDraw()
  window.addEventListener('resize', onResize)
  observer.value = new IntersectionObserver(handleIntersect)
  observer.value.observe(loadMoreTrigger.value)
})

onBeforeUnmount(() => {
  window.removeEventListener('resize', onResize)
})
</script>

重点分析

组件的核心逻辑位于 flowDraw 函数中,它负责计算并绘制每个元素的位置。首先,它根据容器宽度和配置的列宽、列间距计算出列数,并初始化每列的高度为 0。然后,它遍历所有元素,找到当前高度最小的那一列,并将该元素放置在该列的顶部,同时更新该列的高度。最后,它设置容器的宽度和高度,以适应所有元素。

为了提高性能,组件使用了防抖技术处理窗口大小变化事件。当窗口大小发生变化时,它会延迟 300 毫秒后再重新计算布局,避免频繁的重绘和重排。

另一个重要的功能是上拉加载更多。组件使用了 IntersectionObserver API 来监听一个触发元素是否进入视口。当触发元素进入视口时,就会调用 loadMore 函数,该函数会通知父组件加载更多数据,并在新数据到达后重新计算布局。值得注意的是,组件还维护了一个 hasMore 状态,当没有更多数据时,它会禁止继续加载,并在界面上显示相应的提示,没有数据就显示没有更多了...

布局的样式代码,这里留插槽保证可扩展性

xml 复制代码
<template>
  <div ref="wrapper">
    <div ref="content" class="WaterfallContent">
      <div v-for="(item, index) in list" :key="index" class="WaterfallItem">
        <slot name="item" :index="index" :item="item" />
      </div>
    </div>
    <div ref="loadMoreTrigger" class="flex justify-center">
      {{ hasMore ? '加载更多中...' : '没有更多了...' }}
    </div>
  </div>
</template>

<style lang="scss" scoped>
.WaterfallContent {
  margin-left: auto;
  margin-right: auto;
  position: relative;
}
.WaterfallItem {
  padding-bottom: 24px;
  width: v-bind('`${columnWidth}px`');
  position: absolute;
  top: 0;
  left: 0;
}
</style>

注意:

除了功能实现外,这个组件还注重了可扩展性和可维护性。使用了 Vue 的插槽机制,允许父组件自定义每个元素的内容和样式。

同时,它将样式和逻辑分离,使用 Scoped CSS 确保样式的局部作用域,避免样式污染。

组件销毁时记得把监听器取消哦

总结:

总的来说,这个瀑布流组件通过合理的架构设计、高效的算法实现和良好的性能优化,为开发者提供了一种灵活、强大的解决方案。

它不仅能满足基本的瀑布流布局需求,还能通过配置项和插槽机制进行定制化扩展,适用于各种场景。

无论是个人项目还是企业级应用,都可以借助这个组件快速构建出优雅的瀑布流体验。

相关推荐
牧艺34 分钟前
cos-design v3.0:从 15 个 Demo 到 49 个组件的视觉特效库
前端·视觉设计
lichenyang45336 分钟前
ASCF 架构升级总览:WebRuntimePage 为什么要变薄
前端
道友可好36 分钟前
从今天开始:你的第一个 Harness Engineering 实践
前端·人工智能·后端
Linsk38 分钟前
组件 = 模板 + 业务逻辑
java·前端·vue.js
二月龙1 小时前
移动端 H5 页面开发:响应式适配 + 低版本兼容实战指南
前端
小强19881 小时前
HTML5 新表单全解:日期、手机号、颜色选择器
前端
妙码生花1 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(二):目录结构、初始化 GIT、设计并开发配置系统
前端·后端·go
鱼人1 小时前
HTML5 本地存储终极指南
前端
超绝大帅哥2 小时前
React的Fiber是什么? Vue为什么不需要Fiber ?
前端
yingyima2 小时前
正则表达式分组与捕获:凌晨3点服务器报警的解决方案
前端