vue3 写一个简单的瀑布流组件

2,444 阅读1分钟

基本原理

  • 通过元素宽度和容器宽度计算出一排的数量 cols
  • 通过 cols 的长度生成一个 posY 集合 [0, 0, ....]
  • 遍历所有节点
  • 获取 posY 集合的最小值 minY 作为节点的 y 轴偏移,记录 minYminYIndex
  • 通过 minYIndex 以及元素的宽度计算出节点的 curX 值最为节点的 x 轴偏移
  • 更新 posY 集合的最小值,原始高度 minY 加上节点的高度
  • 循环结束后将 posY 集合的最大值 maxY 设置为容器的高度

实现

模版样式

  <div ref="waterfallWrapper" :style="{ height: `${wrapperHeight}px` }">
    <div v-for="item in list" :key="item.id" class="waterfall-item">
      <img :src="item.url">
    </div>
  </div>

将元素绝对定位在 (0, 0) 点,通过 tranlate 实现布局

.waterfall-item {
  position: absolute;
  left: 0;
  top: 0;
}

.waterfall-item img {
  display: black;
}

图片数据

生成了30张大小不一致的图片

const list = ref([])
for (let i = 1; i < 30; i++) {
  list.value.push({
    url: `https://images.weserv.nl/?url=https://api.mz-moe.cn/img/img${i}.jpg`,
  })
}

容器高宽

const waterfallWrapper = ref(null)
const wrapperWidth = ref(0)
const wrapperHeight = ref(0)
onMounted(() => {
  wrapperWidth.value = waterfallWrapper.value.offsetWidth
})

列与偏移

// 间隙
const gutter = ref(20)
// 单列宽
const colWidth = ref(300)
// 列数
const cols = computed(() => {
  return Math.floor((wrapperWidth.value - gutter.value) / (colWidth.value + gutter.value))
})
// x偏移
const offsetX = computed(() => {
  const contextWidth = cols.value * (colWidth.value + gutter.value) + gutter.value
  return (wrapperWidth.value - contextWidth) / 2
})

核心排版

function layout() {
  // 初始化 y 集合
  const posY = new Array(cols.value).fill(gutter.value)
  // 节点
  const items = document.querySelectorAll('.waterfall-item')
  // 遍历
  for (let i = 0; i < items.length; i++) {
    const curItem = items[i]
    // 最小的y值
    const minY = Math.min.apply(null, posY)
    // 最小y的下标
    const minYIndex = posY.indexOf(minY)
    // 当前下标对应的x
    const curX = gutter.value * (minYIndex + 1) + colWidth.value * minYIndex + offsetX.value
    // 设置偏移
    curItem.style.transform = `translate3d(${curX}px,${minY}px, 0)`
    curItem.style.width = `${colWidth.value}px`
    // 更新当前index的y值
    const { height } = curItem.getBoundingClientRect()
    posY[minYIndex] += height + gutter.value
  }

  // 设置容器高度
  wrapperHeight.value = Math.max.apply(null, posY)
}

onMounted(() => {
  layout()
})

图片高度

如果元素里面有图片 img,由于遍历节点的时候图片可能还未加载完成,无法正确的获取到节点的高度,为此我们需要在图片加载完成后重新执行 layout()

img 添加上 @load 方法

  <div ref="waterfallWrapper" :style="{ height: `${wrapperHeight}px` }">
    <div v-for="item in list" :key="item.id" class="waterfall-item">
      <img :src="item.url" @load="imageLoad">
    </div>
  </div>

可以加上防抖避免段时间内多张图片加载完成触发多次重排

const imageLoad = debounce(() => {
    layout()
}, 1000)

这样简单的瀑布流就实现了,效果如下:

1650260936451.jpg

完善的瀑布流插件

支持animate.css动画,响应式,图片懒加载

vue-waterfall-plugin