Vue3实现瀑布流的2种核心方案

 更新时间:2025年12月02日 08:49:23   作者:北极糊的狐  
这篇文章主要介绍了Vue3中实现瀑布流的两种主要方案:CSS原生方案和JS计算方案,CSS方案适用于静态短列表,而JS方案适用于长列表和复杂布局,能够保证排序正常并支持懒加载,文章详细介绍了两种方案的实现步骤,需要的朋友可以参考下

查资料Vue3 中实现瀑布流主要有 CSS 原生方案(简单场景) 和 JS 计算方案(复杂场景),整理了两种方法的实现步骤。

一、方案对比

方案核心原理优点缺点适用场景
CSS 方案column-count/grid 布局代码简单、无 JS 计算排序乱(按列排布)、不支持懒加载静态短列表、简单瀑布流
JS 方案计算列高 + 动态插入排序正常、支持懒加载需 JS 计算、适配窗口变化动态加载、长列表、复杂布局

二、方案 1:CSS 原生实现(快速上手)

1. column 多列布局(最简单)

利用 column-count 拆分列,column-gap 控制列间距,适合静态数据展示。

<template>
  <div class="waterfall-css">
    <div 
      class="waterfall-item" 
      v-for="(item, index) in list" 
      :key="index"
    >
      <img :src="item.img" alt="瀑布流图片" class="item-img" />
      <p class="item-desc">{{ item.desc }}</p>
    </div>
  </div>
</template>
 
<script setup>
import { ref } from 'vue';
 
// 模拟瀑布流数据
const list = ref([
  { img: 'https://picsum.photos/300/400?1', desc: '图片1' },
  { img: 'https://picsum.photos/300/300?2', desc: '图片2' },
  { img: 'https://picsum.photos/300/500?3', desc: '图片3' },
  { img: 'https://picsum.photos/300/200?4', desc: '图片4' },
  { img: 'https://picsum.photos/300/600?5', desc: '图片5' },
  { img: 'https://picsum.photos/300/350?6', desc: '图片6' },
]);
</script>
 
<style scoped>
/* 瀑布流容器 */
.waterfall-css {
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  /* 核心:拆分为3列 */
  column-count: 3;
  /* 列间距 */
  column-gap: 16px;
  padding: 16px;
}
 
/* 瀑布流项(避免跨列断裂) */
.waterfall-item {
  /* 关键:禁止跨列 */
  break-inside: avoid;
  margin-bottom: 16px;
  border-radius: 8px;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  overflow: hidden;
}
 
/* 图片自适应 */
.item-img {
  width: 100%;
  height: auto;
  display: block;
}
 
.item-desc {
  padding: 12px;
  font-size: 14px;
  color: #333;
}
 
/* 响应式适配 */
@media (max-width: 768px) {
  .waterfall-css {
    column-count: 2; /* 移动端2列 */
  }
}
@media (max-width: 480px) {
  .waterfall-css {
    column-count: 1; /* 小屏1列 */
  }
}
</style>

2. CSS Grid 布局(更灵活)

适合需要自定义列宽、对齐方式的场景,兼容现代浏览器。

<template>
  <div class="waterfall-grid">
    <div 
      class="waterfall-item" 
      v-for="(item, index) in list" 
      :key="index"
    >
      <img :src="item.img" alt="" class="item-img" />
    </div>
  </div>
</template>
 
<script setup>
import { ref } from 'vue';
const list = ref([/* 同上文数据 */]);
</script>
 
<style scoped>
.waterfall-grid {
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  padding: 16px;
  /* Grid 核心配置 */
  display: grid;
  /* 3列,列宽自适应,间距16px */
  grid-template-columns: repeat(3, 1fr);
  grid-gap: 16px;
  /* 行高自适应(关键) */
  grid-auto-rows: minmax(100px, auto);
}
 
.waterfall-item {
  border-radius: 8px;
  overflow: hidden;
}
 
.item-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
 
/* 响应式 */
@media (max-width: 768px) {
  .waterfall-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}
</style>

三、方案 2:JS 计算实现(推荐,排序正常)

CSS 方案的缺陷是「元素按列从上到下排布」(而非从左到右),JS 方案通过计算每列高度,将新元素插入最短列,保证排序和布局更符合预期。

完整实现(含懒加载 + 窗口适配)

<template>
  <div class="waterfall-js" ref="waterfallRef">
    <!-- 列容器(动态生成) -->
    <div 
      class="waterfall-column" 
      v-for="(col, index) in columnList" 
      :key="index"
      ref="columnRefs"
    >
      <!-- 列内元素 -->
      <div 
        class="waterfall-item" 
        v-for="item in col" 
        :key="item.id"
        @load="handleImgLoad(item.id)"
      >
        <img 
          :src="item.img" 
          alt="" 
          class="item-img"
          :data-id="item.id"
          loading="lazy" <!-- 原生懒加载 -->
        />
        <p class="item-desc">{{ item.desc }}</p>
      </div>
    </div>
    <!-- 加载更多 -->
    <div class="load-more" v-if="hasMore" @click="loadMore">加载更多</div>
    <div class="no-more" v-else>没有更多数据了</div>
  </div>
</template>
 
<script setup>
import { ref, onMounted, onResize, nextTick } from 'vue';
 
// 核心变量
const waterfallRef = ref(null); // 瀑布流容器
const columnRefs = ref([]); // 列容器引用
const columnCount = ref(3); // 列数(默认3列)
const columnHeights = ref([]); // 每列的高度
const columnList = ref([]); // 每列的元素列表
const hasMore = ref(true); // 是否有更多数据
let page = 1; // 当前页码
 
// 初始化列
const initColumns = () => {
  // 根据屏幕宽度设置列数
  const width = document.documentElement.clientWidth;
  if (width <= 480) {
    columnCount.value = 1;
  } else if (width <= 768) {
    columnCount.value = 2;
  } else {
    columnCount.value = 3;
  }
  // 重置列数据
  columnList.value = Array.from({ length: columnCount.value }, () => []);
  columnHeights.value = Array(columnCount.value).fill(0);
};
 
// 模拟请求数据
const fetchData = async (pageNum) => {
  // 模拟接口延迟
  await new Promise(resolve => setTimeout(resolve, 500));
  const mockData = Array.from({ length: 9 }, (_, i) => ({
    id: `${pageNum}-${i}`,
    img: `https://picsum.photos/300/${200 + Math.random() * 400 | 0}?${pageNum}-${i}`,
    desc: `第${pageNum}页-${i+1}条`
  }));
  return mockData;
};
 
// 分配元素到最短列
const assignToShortestColumn = (data) => {
  data.forEach(item => {
    // 找到最短列的索引
    const minHeightIndex = columnHeights.value.indexOf(Math.min(...columnHeights.value));
    // 插入元素
    columnList.value[minHeightIndex].push(item);
    // 预估值(图片加载前先占位,避免高度计算偏差)
    columnHeights.value[minHeightIndex] += 300; // 临时占位高度
  });
};
 
// 图片加载完成后更新列高度
const handleImgLoad = (id) => {
  nextTick(() => {
    // 找到当前图片元素
    const img = document.querySelector(`img[data-id="${id}"]`);
    if (!img) return;
    const item = img.closest('.waterfall-item');
    const column = item.closest('.waterfall-column');
    // 找到列索引
    const colIndex = columnRefs.value.findIndex(col => col === column);
    if (colIndex === -1) return;
    // 重新计算列高度
    columnHeights.value[colIndex] = column.offsetHeight;
  });
};
 
// 加载更多数据
const loadMore = async () => {
  const data = await fetchData(page);
  if (data.length === 0) {
    hasMore.value = false;
    return;
  }
  assignToShortestColumn(data);
  page++;
};
 
// 窗口大小变化时重新布局
const handleResize = () => {
  initColumns();
  // 重新分配所有数据(合并所有列数据)
  const allData = columnList.value.flat();
  columnList.value = Array.from({ length: columnCount.value }, () => []);
  columnHeights.value = Array(columnCount.value).fill(0);
  assignToShortestColumn(allData);
};
 
// 初始化
onMounted(() => {
  initColumns();
  loadMore();
  // 监听窗口变化(Vue3 内置 onResize)
  onResize(handleResize);
});
</script>
 
<style scoped>
.waterfall-js {
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  padding: 16px;
  /* 弹性布局实现列并排 */
  display: flex;
  gap: 16px; /* 列间距 */
}
 
.waterfall-column {
  /* 列宽均分 */
  flex: 1;
}
 
.waterfall-item {
  margin-bottom: 16px;
  border-radius: 8px;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  overflow: hidden;
}
 
.item-img {
  width: 100%;
  height: auto;
  display: block;
}
 
.item-desc {
  padding: 12px;
  font-size: 14px;
  color: #333;
}
 
/* 加载更多样式 */
.load-more, .no-more {
  width: 100%;
  text-align: center;
  padding: 16px;
  font-size: 14px;
  color: #666;
  cursor: pointer;
}
 
.load-more:hover {
  color: #409eff;
}
 
.no-more {
  cursor: default;
  color: #999;
}
</style>

四、核心优化技巧

1. 图片加载优化

  • 用 loading="lazy" 实现原生懒加载,减少首屏加载压力;
  • 图片加载前设置占位高度(避免列高度计算偏差);
  • 图片加载完成后重新计算列高度(handleImgLoad 方法)。

2. 窗口适配

  • 监听 onResize 事件,动态调整列数并重新分配元素;
  • 用 nextTick 确保 DOM 更新后再计算高度。

3. 性能优化

  • 长列表场景:结合 vue-virtual-scroller 实现虚拟列表,避免 DOM 过多;
  • 数据请求:防抖 / 节流加载更多,避免频繁请求;
  • 样式优化:减少 offsetHeight 等重排操作,批量更新列数据。

4. 兼容处理

  • 低版本浏览器:替换 Array.flat() 等 ES6+ 方法,或引入 polyfill;
  • 图片加载失败:添加 onerror 回调,显示默认占位图。

五、第三方库推荐(懒人方案)

若不想手动实现,可使用成熟的 Vue3 瀑布流组件:

vue3-waterfall-easy:轻量、支持懒加载、响应式,文档友好;

npm install vue3-waterfall-easy --save 

vue-grid-layout:适用于可拖拽的瀑布流布局,支持自定义网格大小;

masonry-layout:经典的瀑布流库,可结合 Vue3 封装使用。

六、总结

  1. 快速实现:用 CSS column-count 或 grid,适合静态短列表;
  2. 生产环境:优先用 JS 计算方案,保证排序正常、支持动态加载;
  3. 核心逻辑:JS 方案的关键是「计算列高 → 插入最短列 → 图片加载后更新高度」;
  4. 优化重点:图片懒加载、窗口适配、减少重排操作。

可根据项目需求选择方案:简单场景用 CSS,复杂场景(动态加载、排序要求)用 JS 方案,或直接使用第三方库提升开发效率。

以上就是Vue3实现瀑布流的2种核心方案的详细内容,更多关于Vue3实现瀑布流的资料请关注脚本之家其它相关文章!

相关文章

  • Vue 路由传参加密的示例代码

    Vue 路由传参加密的示例代码

    这篇文章主要介绍了Vue 路由传参加密,本文结合示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-12-12
  • vuex使用方法超详细讲解(实用)

    vuex使用方法超详细讲解(实用)

    这篇文章主要给大家介绍了关于vuex使用方法的相关资料,主要内容包括Vuex的安装和配置,以及如何在.vue文件中引入和使用Vuex状态,作者还分享了一种在模块中存储状态的建议方法,并提供了具体的代码示例,需要的朋友可以参考下
    2024-10-10
  • vue里使用create,mounted调用方法的正确姿势说明

    vue里使用create,mounted调用方法的正确姿势说明

    这篇文章主要介绍了vue里使用create,mounted调用方法的正确姿势,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-04-04
  • Vue3源码分析侦听器watch的实现原理

    Vue3源码分析侦听器watch的实现原理

    watch 的本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数。watch的实现利用了effect 和 options.scheduler 选项,这篇文章主要介绍了Vue3源码分析侦听器watch的实现原理,需要的朋友可以参考下
    2022-08-08
  • 详解vue-cli@2.x项目迁移日志

    详解vue-cli@2.x项目迁移日志

    这篇文章主要介绍了详解vue-cli@2.x项目迁移日志,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-06-06
  • Vue 前端实现登陆拦截及axios 拦截器的使用

    Vue 前端实现登陆拦截及axios 拦截器的使用

    这篇文章主要介绍了Vue 前端实现登陆拦截及axios 拦截器的使用,通过这个项目学习如何实现一个前端项目中所需要的 登录及拦截、登出、token失效的拦截及对应 axios 拦截器的使用。需要的朋友可以参考下
    2019-07-07
  • vue如何使用cookie、localStorage和sessionStorage进行储存数据

    vue如何使用cookie、localStorage和sessionStorage进行储存数据

    这篇文章主要介绍了vue如何使用cookie、localStorage和sessionStorage进行储存数据,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • vue watch如何深度监听数组每一项的变化

    vue watch如何深度监听数组每一项的变化

    这篇文章主要介绍了vue watch如何深度监听数组每一项的变化问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-07-07
  • 在Vue3中安全访问子组件的示例代码

    在Vue3中安全访问子组件的示例代码

    在 Vue 开发中,父子组件间的通信是高频场景,当需要在父组件中直接调用子组件的方法时,模板引用(Template Refs)是最直接的解决方案,本文将通过一段 Vue 3 代码片段,深入剖析如何通过 TypeScript 实现类型安全的子组件实例访问,需要的朋友可以参考下
    2025-03-03
  • Vue3.x的版本中build后dist文件中出现legacy的js文件问题

    Vue3.x的版本中build后dist文件中出现legacy的js文件问题

    这篇文章主要介绍了Vue3.x的版本中build后dist文件中出现legacy的js文件问题及解决,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-07-07

最新评论