Vue3实现瀑布流的2种核心方案
查资料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 封装使用。
六、总结
- 快速实现:用 CSS
column-count或grid,适合静态短列表; - 生产环境:优先用 JS 计算方案,保证排序正常、支持动态加载;
- 核心逻辑:JS 方案的关键是「计算列高 → 插入最短列 → 图片加载后更新高度」;
- 优化重点:图片懒加载、窗口适配、减少重排操作。
可根据项目需求选择方案:简单场景用 CSS,复杂场景(动态加载、排序要求)用 JS 方案,或直接使用第三方库提升开发效率。
以上就是Vue3实现瀑布流的2种核心方案的详细内容,更多关于Vue3实现瀑布流的资料请关注脚本之家其它相关文章!
相关文章
vue里使用create,mounted调用方法的正确姿势说明
这篇文章主要介绍了vue里使用create,mounted调用方法的正确姿势,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2022-04-04
vue如何使用cookie、localStorage和sessionStorage进行储存数据
这篇文章主要介绍了vue如何使用cookie、localStorage和sessionStorage进行储存数据,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2022-08-08
Vue3.x的版本中build后dist文件中出现legacy的js文件问题
这篇文章主要介绍了Vue3.x的版本中build后dist文件中出现legacy的js文件问题及解决,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2022-07-07


最新评论