瀑布流布局是一种流行的布局方式,常见于图片展示、电商产品列表等场景。
它的核心思想是将元素按照一定规则分配到多列中,每列的高度由其中的元素决定,从而实现了一种类似自然堆砌的视觉效果。
相比传统的网格布局,瀑布流布局更加灵活、动态,能更好地利用空间。
这个瀑布流组件的实现采用了 Vue 3 的 Setup 语法,充分利用了 Composition API 的特性。它定义了一些重要的响应式数据,如 props
用于接收外部传入的列表数据、列宽、列间距等配置项,emits
用于触发自定义事件通知父组件,wrapper
和 content
分别用于获取容器元素和内容元素的引用。
具体的代码实现
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 确保样式的局部作用域,避免样式污染。
组件销毁时记得把监听器取消哦
总结:
总的来说,这个瀑布流组件通过合理的架构设计、高效的算法实现和良好的性能优化,为开发者提供了一种灵活、强大的解决方案。
它不仅能满足基本的瀑布流布局需求,还能通过配置项和插槽机制进行定制化扩展,适用于各种场景。
无论是个人项目还是企业级应用,都可以借助这个组件快速构建出优雅的瀑布流体验。