# 一、瀑布流布局(列数目固定)
瀑布流的实现方式有很多种,可以根据不同的应用场景选择不同的方案:
# 图片高度获取
<script>
new Vue({
el: '#app',
data: {
imgDataList: []
},
async created() {
const { data } = await axios.get('https://picsum.photos/v2/list')
// this.dataList = data
const dataHandle = []
data.forEach((item) => {
let img = new Image()
img.src = item.download_url
img.onload = function () {
item.picWidth = this.width
item.picHeight = this.height
dataHandle.push(item)
}
})
this.imgDataList = dataHandle
}
})
</script>
# 1、多列布局
使用多列布局的特点是,有 CSS 确定固定的列数(column-count)、宽度(column-width)等
特点:先分为固定列数,然后按照一列一列的获取数据
提示
由于它的渲染特性,在正式的开发中不推荐使用(数据非常少,分页式的场景可以尝试使用 😂)。
.waterfall-container {
column-count: 2; /* 列数 */
column-gap: 1vw; /* 列间距 */
/* 防止被错位截断 */
break-inside: avoid;
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
column-fill: balance-all;
}
.waterfall-item {
padding: 2vw;
/* margin: 2vw; */
margin-bottom: 2vw;
/* 防止被错位截断 */
height: 100%;
overflow: auto;
}
# 2、grid 布局 + js ✨
grid 布局和多列布局实现瀑布流布局的方式几乎一致,不同的是,grid 布局渲染的数据是:从左到右依次渲染的。
提示
尽管当前方式可以解决基本的瀑布流布局(特别是移动端)问题,但还是存在不少问题:
- 依旧无法动态的决定使用几列
- 后端得返回图片的 width、height(前端性能消耗会比较大)
- itemAsyncHeight 的计算结果会受到 padding、margin 的影响
.waterfall-container {
display: grid;
grid-template-columns: repeat(2, 1fr); /* 列数和大小 */
grid-gap: 1vw; /* 间隔 */
}
其实我们使直接使用一个
grid-template-rows: masonry;
代替 js 操作,但是目前还是实验性功能。
<div id="app">
<h1>{{nickName}}</h1>
<div class="waterfall-container">
<div class="left-column">
<!-- …… -->
</div>
<div class="right-column">
<!-- …… -->
</div>
</div>
</div>
// 也可以封装成一个class类
function getPicHeight(picWidth, picHeight) {
const screenWidth = window.innerWidth // 屏幕宽度
const itemPadding = 15
const itemMargin = 5
const gridGap = 5
const gridColumns = 2
// 1、获取 item 的视图宽度
const itemAsyncWidth = ~~(
(screenWidth - (itemPadding + itemMargin) * gridColumns * 2 - gridGap * (gridColumns - 1)) /
gridColumns
) // 取整
// 2、获取 item 的视图高度
const itemAsyncHeight = ((picHeight / picWidth) * itemAsyncWidth).toFixed(0) // 取整
return itemAsyncHeight
}
# 3、flex 布局 + js
和上面的 grid 布局 + js 实现一样,但它比前者要弱一点的是:对 columns 列数、gap 间隙的管理更加不便。
# 4、绝对定位 + js
使用绝对定位 + js 方案,实现了对 columns 列数的统一管理,可以适用于 pc 端使用。其缺点是,由于 columns 固定,响应式实现性能消耗较大。
.waterfall-container {
position: relative;
width: 100%;
height: 100%;
}
.waterfall-item {
position: absolute;
height: 100%;
}
核心实现
new Vue({
el: '#app',
data: {
title: '瀑布流-绝对定位',
dataList: [],
waterfallColumns: 3,
waterfallGap: 0
},
methods: {
getPicHeight(picWidth, picHeight) {
const screenWidth = window.innerWidth // 屏幕宽度
// 1、获取 item 的视图宽度
const itemAsyncWidth = ~~(
(screenWidth - this.waterfallGap * (this.waterfallColumns - 1)) /
this.waterfallColumns
) // 取整
// 2、获取 item 的视图高度
const itemAsyncHeight = (picHeight / picWidth) * itemAsyncWidth // 取整
return itemAsyncHeight
},
picPositionHandle(data) {
if (data.length > 0) {
let columnsHeightArr = new Array(this.waterfallColumns).fill(0)
this.dataList = []
data.forEach((item, index) => {
let arrIndex = columnsHeightArr.indexOf(Math.min.apply(null, columnsHeightArr))
// item 配置
item.styleWidth = 100 / this.waterfallColumns + 'vw'
item.styleLeft = arrIndex * (100 / this.waterfallColumns) + 'vw'
item.styleTop = columnsHeightArr[arrIndex] + 'px'
// 列高叠加
let columnsHeight = this.getPicHeight(item.width, item.height)
columnsHeightArr[arrIndex] += columnsHeight
this.dataList.push(item)
})
}
},
handleResize() {
// 更新屏幕宽度和高度数据
this.picPositionHandle(this.dataList)
}
},
async created() {
const { data } = await axios.get('https://picsum.photos/v2/list')
// this.dataList = data;
this.picPositionHandle(data)
},
mounted() {
// 添加窗口大小改变事件监听器
window.addEventListener('resize', this.handleResize)
}
})
# 4、绝对定位 + js + flex + 等宽 ✨
这个是在上个方案的基础上进行改造的。
.waterfall-container {
display: flex;
flex-wrap: wrap;
position: relative;
width: 100%;
height: 100%;
}
.waterfall-item {
position: absolute;
height: 100%;
}
核心实现
new Vue({
el: '#app',
data: {
title: '瀑布流-绝对定位-flex-等宽',
dataList: [],
styleWidth: 200,
waterfallGap: 0
},
methods: {
picPositionHandle(data) {
if (data.length > 0) {
const screenWidth = window.innerWidth // 屏幕宽度
const waterfallColumns = Math.floor(screenWidth / 200)
let columnsHeightArr = new Array(waterfallColumns).fill(0)
this.dataList = []
data.forEach((item, index) => {
let arrIndex = columnsHeightArr.indexOf(Math.min.apply(null, columnsHeightArr))
// item 配置
item.styleLeft = arrIndex * (100 / waterfallColumns) + 'vw'
item.styleTop = columnsHeightArr[arrIndex] + 'px'
// 列高叠加
let columnsHeight = Number(((item.height / item.width) * 200).toFixed(0))
columnsHeightArr[arrIndex] += columnsHeight
this.dataList.push(item)
})
}
},
handleResize() {
// 更新屏幕宽度和高度数据
this.picPositionHandle(this.dataList)
}
},
async created() {
const { data } = await axios.get('https://picsum.photos/v2/list')
// this.dataList = data;
this.picPositionHandle(data)
},
mounted() {
// 添加窗口大小改变事件监听器
window.addEventListener('resize', this.handleResize)
}
})
# 5、开源库
https://github.com/jiaozitang/react-masonry-component2#readme
# 二、首屏渲染优化
# 1、懒加载 🎈
实现懒加载通常有两种方式,一种是自己计算可视窗口与图片的距离并监听,另外一种是使用 IntersectionObserver (opens new window)
<div class="waterfall-container">
<template v-for="item in dataList" :key="item.id">
<div class="waterfall-item ">
<!-- 这里 -->
<img src="../public/default.png" :data-src="item.download_url" alt="" />
</div>
</template>
</div>
简单实现
new Vue({
async created() {
// ……
// =============
// 图片懒加载
this.$nextTick(() => {
const interSectObs = new IntersectionObserver(
(entries) => {
for (let entry of entries) {
// 1、交叉了
if (entry.isIntersecting) {
const imgElemt = entry.target
imgElemt.src = imgElemt.dataset.src // 加载图片
interSectObs.unobserve(imgElemt)
}
}
},
{ threshold: 0 }
)
const imgs = document.querySelectorAll('img[data-src]')
imgs.forEach((img) => {
console.log(img)
interSectObs.observe(img)
})
})
}
})
自定义指令封装版
<div class="waterfall-container">
<template v-for="item in dataList" :key="item.id">
<div class="waterfall-item ">
<!-- 这里 -->
<img
v-imglazy="{ islazy }"
src="../public/default.png"
:data-src="item.download_url"
alt=""
/>
</div>
</template>
</div>
Vue.directive('imglazy', function (el, binding) {
const { islazy } = binding.value
if (!islazy) return (el.src = el.dataset.src)
const interSectObs = new IntersectionObserver((entries) => {
if (entries[0].intersectionRatio <= 0) return
el.src = el.dataset.src
el.unobserve(imgElemt)
})
interSectObs.observe(el)
})
# 2、加动画
以使用 Animate.css 为例:
<div class="waterfall-container">
<template v-for="item in dataList" :key="item.id">
<!-- 这里 -->
<div class="waterfall-item animate__animated animate__bounceIn animate__delay-2s">
<img :src="item.download_url" alt="" />
</div>
</template>
</div>
# 3、并行渲染
简单一句话:同时加载多个图片资源并在加载完成后显示它们