Vue3實(shí)現(xiàn)瀑布流的2種核心方案
查資料Vue3 中實(shí)現(xiàn)瀑布流主要有 CSS 原生方案(簡(jiǎn)單場(chǎng)景) 和 JS 計(jì)算方案(復(fù)雜場(chǎng)景),整理了兩種方法的實(shí)現(xiàn)步驟。
一、方案對(duì)比
| 方案 | 核心原理 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場(chǎng)景 |
|---|---|---|---|---|
| CSS 方案 | column-count/grid 布局 | 代碼簡(jiǎn)單、無(wú) JS 計(jì)算 | 排序亂(按列排布)、不支持懶加載 | 靜態(tài)短列表、簡(jiǎn)單瀑布流 |
| JS 方案 | 計(jì)算列高 + 動(dòng)態(tài)插入 | 排序正常、支持懶加載 | 需 JS 計(jì)算、適配窗口變化 | 動(dòng)態(tài)加載、長(zhǎng)列表、復(fù)雜布局 |
二、方案 1:CSS 原生實(shí)現(xiàn)(快速上手)
1. column 多列布局(最簡(jiǎn)單)
利用 column-count 拆分列,column-gap 控制列間距,適合靜態(tài)數(shù)據(jù)展示。
<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';
// 模擬瀑布流數(shù)據(jù)
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;
}
/* 瀑布流項(xiàng)(避免跨列斷裂) */
.waterfall-item {
/* 關(guān)鍵:禁止跨列 */
break-inside: avoid;
margin-bottom: 16px;
border-radius: 8px;
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
}
/* 圖片自適應(yīng) */
.item-img {
width: 100%;
height: auto;
display: block;
}
.item-desc {
padding: 12px;
font-size: 14px;
color: #333;
}
/* 響應(yīng)式適配 */
@media (max-width: 768px) {
.waterfall-css {
column-count: 2; /* 移動(dòng)端2列 */
}
}
@media (max-width: 480px) {
.waterfall-css {
column-count: 1; /* 小屏1列 */
}
}
</style>2. CSS Grid 布局(更靈活)
適合需要自定義列寬、對(duì)齊方式的場(chǎng)景,兼容現(xiàn)代瀏覽器。
<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([/* 同上文數(shù)據(jù) */]);
</script>
<style scoped>
.waterfall-grid {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 16px;
/* Grid 核心配置 */
display: grid;
/* 3列,列寬自適應(yīng),間距16px */
grid-template-columns: repeat(3, 1fr);
grid-gap: 16px;
/* 行高自適應(yīng)(關(guān)鍵) */
grid-auto-rows: minmax(100px, auto);
}
.waterfall-item {
border-radius: 8px;
overflow: hidden;
}
.item-img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 響應(yīng)式 */
@media (max-width: 768px) {
.waterfall-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>三、方案 2:JS 計(jì)算實(shí)現(xiàn)(推薦,排序正常)
CSS 方案的缺陷是「元素按列從上到下排布」(而非從左到右),JS 方案通過(guò)計(jì)算每列高度,將新元素插入最短列,保證排序和布局更符合預(yù)期。
完整實(shí)現(xiàn)(含懶加載 + 窗口適配)
<template>
<div class="waterfall-js" ref="waterfallRef">
<!-- 列容器(動(dòng)態(tài)生成) -->
<div
class="waterfall-column"
v-for="(col, index) in columnList"
:key="index"
ref="columnRefs"
>
<!-- 列內(nèi)元素 -->
<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>沒(méi)有更多數(shù)據(jù)了</div>
</div>
</template>
<script setup>
import { ref, onMounted, onResize, nextTick } from 'vue';
// 核心變量
const waterfallRef = ref(null); // 瀑布流容器
const columnRefs = ref([]); // 列容器引用
const columnCount = ref(3); // 列數(shù)(默認(rèn)3列)
const columnHeights = ref([]); // 每列的高度
const columnList = ref([]); // 每列的元素列表
const hasMore = ref(true); // 是否有更多數(shù)據(jù)
let page = 1; // 當(dāng)前頁(yè)碼
// 初始化列
const initColumns = () => {
// 根據(jù)屏幕寬度設(shè)置列數(shù)
const width = document.documentElement.clientWidth;
if (width <= 480) {
columnCount.value = 1;
} else if (width <= 768) {
columnCount.value = 2;
} else {
columnCount.value = 3;
}
// 重置列數(shù)據(jù)
columnList.value = Array.from({ length: columnCount.value }, () => []);
columnHeights.value = Array(columnCount.value).fill(0);
};
// 模擬請(qǐng)求數(shù)據(jù)
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}頁(yè)-${i+1}條`
}));
return mockData;
};
// 分配元素到最短列
const assignToShortestColumn = (data) => {
data.forEach(item => {
// 找到最短列的索引
const minHeightIndex = columnHeights.value.indexOf(Math.min(...columnHeights.value));
// 插入元素
columnList.value[minHeightIndex].push(item);
// 預(yù)估值(圖片加載前先占位,避免高度計(jì)算偏差)
columnHeights.value[minHeightIndex] += 300; // 臨時(shí)占位高度
});
};
// 圖片加載完成后更新列高度
const handleImgLoad = (id) => {
nextTick(() => {
// 找到當(dāng)前圖片元素
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;
// 重新計(jì)算列高度
columnHeights.value[colIndex] = column.offsetHeight;
});
};
// 加載更多數(shù)據(jù)
const loadMore = async () => {
const data = await fetchData(page);
if (data.length === 0) {
hasMore.value = false;
return;
}
assignToShortestColumn(data);
page++;
};
// 窗口大小變化時(shí)重新布局
const handleResize = () => {
initColumns();
// 重新分配所有數(shù)據(jù)(合并所有列數(shù)據(jù))
const allData = columnList.value.flat();
columnList.value = Array.from({ length: columnCount.value }, () => []);
columnHeights.value = Array(columnCount.value).fill(0);
assignToShortestColumn(allData);
};
// 初始化
onMounted(() => {
initColumns();
loadMore();
// 監(jiān)聽(tīng)窗口變化(Vue3 內(nèi)置 onResize)
onResize(handleResize);
});
</script>
<style scoped>
.waterfall-js {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 16px;
/* 彈性布局實(shí)現(xiàn)列并排 */
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>四、核心優(yōu)化技巧
1. 圖片加載優(yōu)化
- 用
loading="lazy"實(shí)現(xiàn)原生懶加載,減少首屏加載壓力; - 圖片加載前設(shè)置占位高度(避免列高度計(jì)算偏差);
- 圖片加載完成后重新計(jì)算列高度(
handleImgLoad方法)。
2. 窗口適配
- 監(jiān)聽(tīng)
onResize事件,動(dòng)態(tài)調(diào)整列數(shù)并重新分配元素; - 用
nextTick確保 DOM 更新后再計(jì)算高度。
3. 性能優(yōu)化
- 長(zhǎng)列表場(chǎng)景:結(jié)合
vue-virtual-scroller實(shí)現(xiàn)虛擬列表,避免 DOM 過(guò)多; - 數(shù)據(jù)請(qǐng)求:防抖 / 節(jié)流加載更多,避免頻繁請(qǐng)求;
- 樣式優(yōu)化:減少
offsetHeight等重排操作,批量更新列數(shù)據(jù)。
4. 兼容處理
- 低版本瀏覽器:替換
Array.flat()等 ES6+ 方法,或引入 polyfill; - 圖片加載失?。禾砑?nbsp;
onerror回調(diào),顯示默認(rèn)占位圖。
五、第三方庫(kù)推薦(懶人方案)
若不想手動(dòng)實(shí)現(xiàn),可使用成熟的 Vue3 瀑布流組件:
vue3-waterfall-easy:輕量、支持懶加載、響應(yīng)式,文檔友好;
npm install vue3-waterfall-easy --save
vue-grid-layout:適用于可拖拽的瀑布流布局,支持自定義網(wǎng)格大小;
masonry-layout:經(jīng)典的瀑布流庫(kù),可結(jié)合 Vue3 封裝使用。
六、總結(jié)
- 快速實(shí)現(xiàn):用 CSS
column-count或grid,適合靜態(tài)短列表; - 生產(chǎn)環(huán)境:優(yōu)先用 JS 計(jì)算方案,保證排序正常、支持動(dòng)態(tài)加載;
- 核心邏輯:JS 方案的關(guān)鍵是「計(jì)算列高 → 插入最短列 → 圖片加載后更新高度」;
- 優(yōu)化重點(diǎn):圖片懶加載、窗口適配、減少重排操作。
可根據(jù)項(xiàng)目需求選擇方案:簡(jiǎn)單場(chǎng)景用 CSS,復(fù)雜場(chǎng)景(動(dòng)態(tài)加載、排序要求)用 JS 方案,或直接使用第三方庫(kù)提升開(kāi)發(fā)效率。
以上就是Vue3實(shí)現(xiàn)瀑布流的2種核心方案的詳細(xì)內(nèi)容,更多關(guān)于Vue3實(shí)現(xiàn)瀑布流的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue Element-ui表格實(shí)現(xiàn)樹(shù)形結(jié)構(gòu)表格
這篇文章主要為大家詳細(xì)介紹了vue Element-ui表格實(shí)現(xiàn)樹(shù)形結(jié)構(gòu)表格,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-06-06
vue修改數(shù)據(jù)頁(yè)面無(wú)效的解決方案
這篇文章主要介紹了vue修改數(shù)據(jù)頁(yè)面無(wú)效的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06
Vue實(shí)現(xiàn)路由守衛(wèi)的示例代碼
Vue路由守衛(wèi)是控制頁(yè)面導(dǎo)航的鉤子函數(shù),主要用于鑒權(quán)、數(shù)據(jù)預(yù)加載等場(chǎng)景,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-09-09
在Vue3中實(shí)現(xiàn)一個(gè)自定義的v-html組件方式
文章介紹了Vue.js中的v-html指令及其潛在的安全風(fēng)險(xiǎn),然后通過(guò)DOMParser實(shí)現(xiàn)了一個(gè)自定義的v-html組件,以提高安全性并避免XSS攻擊2025-11-11
vue里的axios如何獲取本地json數(shù)據(jù)
這篇文章主要介紹了vue里的axios如何獲取本地json數(shù)據(jù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08

