「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」
前言
继上一篇在Taro小程序中使用直播组件的注意事项,然后在项目中又出现了另一个比较大众化的一个需求(不知道什么时候起出现的瀑布流)大众化???? 哈哈~ 的确
瀑布流:指的是瀑布流式布局、多栏式布局,算是目前比较流行的一种布局方式,给人以参差不齐的感觉,但是又错中有序,类似很多课本或者报纸的牌面,可以大限度的节省空间吧,个人理解~
传统布局
传统布局,如果要实现多栏布局我们能想到float、flex、position等等,几乎所有布局都能基于他们实现,但是,如果是瀑布流,就会显得很棘手~
多栏布局
原理:多栏布局column-count可以指定排列的列数,元素会自动排列生成对应列数瀑布流布局
// waterfall.tsx
<div className="water-fall">
{
list.map((item, idx) => {
return <div key={ idx }>
<img src={ item.url } alt=""/>
<p>{ item.name }</p>
<span>{ item.username }</span>
</div>;
})
}
</div>
.water-fall{
column-count: 2;
column-gap: 10px;
}
.water-fall > div{
break-inside: avoid
}
上述代码是不是很简单,有木有~
column-count注意事项
- 动态添加数据后,column-count会重新排列,重新计算对应列数所需要的数据,导致图片位置会发生变化,导致页面不停跳动,所以只适合一次性拿到所有的数据
- 需要适当考虑兼容性问题,特别是PC端
grid 布局
强大的grid布局,具体可查看阮一峰grid布局讲解,grid布局也可实现瀑布流,动态瀑布流也需要计算起offset偏移量,也需要考虑兼容,实现也很棘手~
重头戏
上述的几种布局,几乎都是css单独就可以实现,其他会有很大的限制,在特定场景或许能满足我们需求,不够通用,因此,建议我们还是通过js的方式实现,通过计算数据中的图片,分成多栏,这样不需要考虑兼容,在支持js平台都能跑通~
实现难点
- 如何计算每一栏总的高度,将小的卡片放到最小栏里边
- 如何计算图片高度(不算难点)
实现
目前前端非常内卷,动不动就是npm install、源码、Class等等,那我们用Class 来实现
// Waterfall.ts
// 首先定义类型(大可不必)any大法不香??
interface IColumnItem {
width: number;
}
// constructor 初始化参数 columns 对象,图片资源字段
interface IOptions {
columns: Array<IColumnItem[]>;
imageUrlField: string;
}
class Waterfall {
protected columns: Array<any>;
protected formattedData: Array<any>;
protected imageUrlField: string;
constructor(options: IOptions) {
this.columns = options.columns || [];
this.imageUrlField = options.imageUrlField || 'url';
// 列对象 包含每列的总height、width
this.formattedData = [];
// 初始化瀑布流 即初始化有几栏
this.initWaterfall(options);
}
/**
* 初始化瀑布流
* @param {Object} options
* @param {Array<Object>} options.columns {width} 定义每列的宽度
*/
initWaterfall(options) {
for (let column of options.columns) {
this.formattedData.push({
width: column.width,
height: 0
});
}
}
/**
* 获取图片资源信息
* @param {Object} resource - 单项资源数据
*/
getResourceInfo(resource) {
return new Promise(resolve => {
if (resource[this.imageUrlField]) {
/**
* 这里没做环境区分,担心过于太多平台 自行区分,eg:(微信小程序、支付宝、h5 )等等
* 如果图片资源太多,建议加上loading,阻止用户操作
*/
// 在浏览器
const img = new Image();
img.src = resource[this.imageUrlField];
img.onload = () => resolve({ width: img.width, height: img.height });
img.onerror = () => resolve(null);
// 在微信小程序(解开注释)
/*wx.getImageInfo({
src: resource[this.imageUrlField],
success(res) {
resolve(res);
},
fail() {
resolve();
}
});*/
} else {
resolve(null);
}
});
}
/**
* 向瀑布流中补充数据
* @param {Boolean} extraVal 瀑布流的卡片中图片之外的内容
* @param {Array<resource>} resources
*/
async addResources(resources, extraVal) {
let newResourcesArray = Array.from({ length: this.columns.length }).map(
_ => []
);
for (let resource of resources) {
let imageInfo;
imageInfo = await this.getResourceInfo(resource);
// const width = Number(resource.width) || 100;
const width = imageInfo.width || 100;
// const height = Number(resource.height) || 100;
const height = imageInfo.height || 100;
// 获取最短列的索引
const index = this.getShortestColumn();
// 卡片宽度
const imageWidth = this.formattedData[index].width;
// 附加的卡片高度在这里是写死的,每个项目会有不同高度
const extraHeight = extraVal ? extraVal : 0;
// 获取卡片高度
const cardHeight = imageWidth / (width / height) + extraHeight;
// 当前列总高度
this.formattedData[index].height += cardHeight;
// 返回卡片高度
resource.cardHeight = cardHeight;
// 返回图片高度
resource.computedHeight = cardHeight - extraVal;
// 返回当前列的数组
newResourcesArray[index].push(resource);
}
return newResourcesArray;
}
/**
* 最重要的地方
* 获取height最小的栏
* 返回height最小栏的索引
*/
getShortestColumn() {
let minHeight = Infinity;
let minHeightColumnIndex = 0;
for (let index = this.formattedData.length - 1; index > -1; index--) {
//从后往前遍历,防止每列的高度相同的情况
const height = this.formattedData[index].height;
if (height <= minHeight) {
minHeightColumnIndex = index;
minHeight = height;
}
}
return minHeightColumnIndex;
}
}
export default Waterfall;
如何使用
import React, { useEffect, useState, useRef } from 'react';
import { Waterfall } from '@/utils';
import data from '../config/data.json';
import './water-fall.css';
const WaterFall = () => {
const [ list, setList ] = useState<any>([]);
const waterFallRef = useRef<any>(null);
useEffect(() => {
const windowWidth = window.innerWidth;
waterFallRef.current = new Waterfall({ columns: [ { width: windowWidth / 2 }, { width: windowWidth / 2 } ] });
waterFallRef.current.addResources(data.payload.list, 50).then(res => {
// 返回计算好的 对应栏数据 剩下的布局常规布局就好了
console.log(res);
setList(res);
});
}, [ data ]);
// 加载更多(调佣addResources方法会自动处理生成瀑布流)
const loadMore = () => {
waterFallRef.current.addResources(data.payload.list, 50).then(res => {
console.log(res);
setList(res);
});
};
return (
<>
/* 数据已经分成 对应栏的数据 正常循环即可*/
<div className="water-fall">
{
list && list.length > 0 && list.map((res, idx) => {
return (
<div key={ idx } className={`column-item column${idx+1}`}>
{
res?.map((item, idx1) => {
return (
<div key={ idx1 } className='work-item'>
<img src={ item.url } alt=""/>
<p>{ item.name }</p>
<span>{ item.username }</span>
</div>
);
})
}
</div>
);
})
}
</div>
<button onClick={ loadMore }>loading</button>
</>
);
};
结果展示:
建议后台直接返回图片资源的width、height这样省去了获取资源的时间,如果非要通过我们自己的方法获取资源图片的信息,前提资源图片不是很大,不然一次加载很多资源,每个资源挨个获取信息会导致等待时间过长
结尾
在此瀑布流js实现方式就已经完成了,代码中有一个获取图片资源的,最好是根据使用场景进行区分,因此那块没有加具体的实现,只是实现了在浏览器中,如果在小程序就用小程序获取图片资源的Api