前言
前端的同学在开发过程中,会遇到瀑布流这种布局方式,特别是商城类、社交类的项目,会很经常遇到。
先明确需求,再选择适合的方法,去渲染页面。
文章目录
- 前言
-
- 一、什么是瀑布流?
- [二、实现方式: 通过 css 样式绘制瀑布流](#二、实现方式: 通过 css 样式绘制瀑布流)
-
-
- [1.column-count 多列布局](#1.column-count 多列布局)
- [2. grid 网格布局 (网页版)](#2. grid 网格布局 (网页版))
-
- [三、 flex 分栏 + JS](#三、 flex 分栏 + JS)
-
-
- 1.根据index奇偶分为两列
- [2. 估算item的高度,将item放入更短的列](#2. 估算item的高度,将item放入更短的列)
-
- [四、scroll-view 结合 grid-builder](#四、scroll-view 结合 grid-builder)
一、什么是瀑布流?
瀑布流
是一种非对称的纵向内容布局方式,核心特点是内容按列排列,每列高度随自身内容自动调整,新内容会不断加载到当前最短的列下方,视觉上像瀑布一样自然向下流动,无需传统分页操作。
这里举一个很经典的例子------小红书:
如图所示,从用户实际体验来看,它有两个最直观的感受:
- "无限滑" 的浏览:不用点击 "下一页",只要向下滚动屏幕,新内容就会自动加载出来,适合碎片化、沉浸式浏览(比如刷小红书笔记、淘宝商品、图片社区内容)。
- 内容不被 "挤压":图片、卡片等内容会按原始比例显示(比如竖版长图、方形图各展所长),不会被强行拉成统一尺寸,视觉上错落有致,重点更突出内容本身。
简单说,它就像把内容 "自然堆叠" 在多列里,列的高度随内容 "长",用户下滑就能不断看到新内容,是现在很多内容 / 电商类平台常用的布局形式。
二、实现方式: 通过 css 样式绘制瀑布流
1.column-count 多列布局
利用 CSS 多列布局(column-count
)自动将内容分配到指定列数,内容会按 "从上到下、从左到右" 的顺序填充,实现基础瀑布流效果。
核心原理:
column-count
: N:指定布局为 N 列。column-gap
: Xpx:设置列之间的间距。- 子元素(如卡片、图片)会自动按顺序填充到各列,列高随内容自然撑开。
效果图:
可以直接复制在本地运行看一下效果哦~
typescript
<template>
<view class="demo-container">
<view class="waterfall">
<view class="item" v-for="item in items" :key="item.id" @click="navigateToDetail(item)">
<image :src="item.image" mode="widthFix" />
<text class="title">{{ item.title }}</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
// Mock数据
const items = ref([
{ id: 1, title: 'Item 1', image: 'https://picsum.photos/200/300' },
{ id: 2, title: 'Item 2', image: 'https://picsum.photos/200/250' },
{ id: 3, title: 'Item 3', image: 'https://picsum.photos/200/350' },
{ id: 4, title: 'Item 4', image: 'https://picsum.photos/200/280' },
{ id: 5, title: 'Item 5', image: 'https://picsum.photos/200/320' },
{ id: 6, title: 'Item 6', image: 'https://picsum.photos/200/260' },
{ id: 7, title: 'Item 7', image: 'https://picsum.photos/200/300' },
{ id: 8, title: 'Item 8', image: 'https://picsum.photos/200/290' },
])
onMounted(() => {
// 可以在这里添加更多数据或进行其他初始化操作
})
const navigateToDetail = (item: any) => {
console.log('navigateToDetail', item)
}
</script>
<style lang="scss">
.demo-container {
padding: 10px;
}
.waterfall {
column-count: 2;
column-gap: 10px;
}
.item {
break-inside: avoid;
margin-bottom: 10px;
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
image {
width: 100%;
display: block;
}
.title {
padding: 8px;
font-size: 14px;
color: #333;
}
}
</style>
特点:
- 优点:实现简单,纯 CSS 无需 JS,自动适应列数,响应式友好。
- 缺点:内容排列顺序是 "从上到下、从左到右"(先填满第一列再填第二列),而非 "优先填充最短列",视觉上列高差异可能较大;动态加载新内容时,新内容会从最后一列继续填充,可能破坏布局平衡。
2. grid 网格布局 (网页版)
利用 CSS Grid
布局,结合 grid-template-rows: masonry
( masonry
网格)实现 "优先填充最短列" 的瀑布流,更符合传统瀑布流的视觉效果(需注意浏览器兼容性)。
核心原理:
display: grid
:启用网格布局。grid-template-columns: repeat(N, 1fr)
:设置 N 列,每列宽度平均分配。grid-template-rows: masonry
:关键属性,启用 "masonry
布局",让每行高度随内容动态调整,优先填充最短列(类似瀑布流)。
这里的图片是在小程序上写了展示的,大家看一下就行。
特点:
- 优点:内容会优先填充最短列,列高更平衡,视觉效果更接近理想瀑布流;支持动态添加内容时自动平衡布局。
- 缺点:
grid-template-rows: masonry
是较新的CSS
特性(2023 年起主流浏览器逐步支持),低版本浏览器(如Safari 16
及以下)可能不兼容,需谨慎使用(可通过caniuse
查看兼容性)。
三、 flex 分栏 + JS
1.根据index奇偶分为两列
这个就简单说一下,一般很少会用这样的方法,除非有特殊需求。
将列表分为两列,js中根据index,遍历出left列和right列。
typescript
// 样式代码
.waterfall-container {
display: flex;
padding: 16rpx;
gap: 16rpx; /* 列间距 */
}
.waterfall-column {
flex: 1; /* 两列宽度相等 */
}
// js方法
/**
* 将数组按索引奇偶性分为两列
* @param arr 源数组(可传入任意类型的数组)
* @returns 包含左列(偶数索引)和右列(奇数索引)的对象
*/
const splitArrayByIndex = () => {
// 初始化左右两列数组
let left = []; // 存放偶数索引元素(index % 2 === 0)
let right = []; // 存放奇数索引元素(index % 2 === 1)
// 遍历数组,根据索引奇偶性分配元素
arr.forEach((item, index) => {
if (index % 2 === 0) {
left.push(item);
} else {
right.push(item);
}
});
return { left, right };
}
特点:
- 优点:emm 简单?
- 缺点:只能奇偶列出,不够灵活。
2. 估算item的高度,将item放入更短的列
直接来代码,样式都倒差不差。
typescript
// html 代码
<view class="waterfall-layout">
<view class="column">
<YourCard v-for="card in list.left" :key="card.id" :item="card" />
</view>
<view class="column">
<YourCard v-for="card in list.left" :key="card.id" :item="card" />
</view>
</view>
// css代码
.waterfall-layout {
width: fit-content;
overflow-y: auto;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
align-items: start;
.column {
width: fit-content;
}
}
// js代码
const splitArrayByHeight = (arr: any[]) => {
let leftHeight = 0
let rightHeight = 0
const left = []
const right = []
arr.forEach(item => {
// 根据内容估算高度(需根据实际内容调整系数)
const itemHeight = item.resource ? 320 : 160 // 假设有图片的卡片更高
if (leftHeight <= rightHeight) {
left.push(item)
leftHeight += itemHeight
} else {
right.push(item)
rightHeight += itemHeight
}
})
return [left, right]
}
特点:
- 优点:更加灵活,能满足大部分的需求场景;
- 缺点:对于不同高度的图片,适应性没那么好,有待优化,优化之后估计会更好用一些。
四、scroll-view 结合 grid-builder
文档直通车:
瀑布流
typescript
<template>
<view class="demo-container">
<scroll-view
class="scroll-container"
scroll-y="true"
@scrolltolower="loadMore"
>
<view class="waterfall-container">
<view class="column" v-for="(column, columnIndex) in columns" :key="columnIndex">
<view @click="navigateToDetail(item)" class="item"
v-for="(item, index) in column"
:key="index"
:style="{ height: item.height + 'px' }"
>
<image :src="item.image" mode="widthFix" />
<text class="title">{{ item.title }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
// Mock数据生成函数
const generateMockData = (count: number) => {
return Array.from({ length: count }, (_, i) => ({
id: i,
title: `Item ${i}`,
image: `https://picsum.photos/200/${200 + Math.floor(Math.random() * 100)}`,
height: 200 + Math.floor(Math.random() * 100)
}))
}
const columns = ref([[], []]) // 两列瀑布流
const loading = ref(false)
// 分配数据到列
const distributeItems = (items: any[]) => {
items.forEach(item => {
// 找到当前高度较小的列
const minHeightColumn = columns.value.reduce((prev, curr, index) => {
const prevHeight = prev.reduce((sum, item) => sum + item.height, 0)
const currHeight = curr.reduce((sum, item) => sum + item.height, 0)
return currHeight < prevHeight ? curr : prev
})
minHeightColumn.push(item)
})
}
const navigateToDetail = (item: any) => {
console.log('navigateToDetail', item)
}
// 加载更多数据
const loadMore = () => {
if (loading.value) return
loading.value = true
setTimeout(() => {
const newData = generateMockData(10)
distributeItems(newData)
loading.value = false
}, 1000)
}
onMounted(() => {
// 初始加载数据
const initialData = generateMockData(20)
distributeItems(initialData)
})
</script>
<style lang="scss">
.demo-container {
width: 100%;
height: 100vh;
}
.scroll-container {
height: 100%;
}
.waterfall-container {
display: flex;
padding: 10px;
gap: 10px;
}
.column {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
}
.item {
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
image {
width: 100%;
display: block;
}
.title {
padding: 8px;
font-size: 14px;
color: #333;
}
}
</style>
特点:
- 优点:组件很直接就能使用;
- 缺点:可能不太稳定,具体看微信开发者社区,有些同学使用起来会出现没有正确渲染的问题。
总结
大家选择其中一个适合自己需求的来写就行,有更好的方法请分享!!!