js 瀑布流万能公式????

552 阅读4分钟

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

前言

继上一篇在Taro小程序中使用直播组件的注意事项,然后在项目中又出现了另一个比较大众化的一个需求(不知道什么时候起出现的瀑布流)大众化???? 哈哈~ 的确

瀑布流:指的是瀑布流式布局、多栏式布局,算是目前比较流行的一种布局方式,给人以参差不齐的感觉,但是又错中有序,类似很多课本或者报纸的牌面,可以大限度的节省空间吧,个人理解~

传统布局

传统布局,如果要实现多栏布局我们能想到floatflexposition等等,几乎所有布局都能基于他们实现,但是,如果是瀑布流,就会显得很棘手~

多栏布局

原理:多栏布局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
}

image.png 上述代码是不是很简单,有木有~

column-count注意事项

  1. 动态添加数据后,column-count会重新排列,重新计算对应列数所需要的数据,导致图片位置会发生变化,导致页面不停跳动,所以只适合一次性拿到所有的数据
  2. 需要适当考虑兼容性问题,特别是PC端
grid 布局

强大的grid布局,具体可查看阮一峰grid布局讲解,grid布局也可实现瀑布流,动态瀑布流也需要计算起offset偏移量,也需要考虑兼容,实现也很棘手~

重头戏

上述的几种布局,几乎都是css单独就可以实现,其他会有很大的限制,在特定场景或许能满足我们需求,不够通用,因此,建议我们还是通过js的方式实现,通过计算数据中的图片,分成多栏,这样不需要考虑兼容,在支持js平台都能跑通~

实现难点

  1. 如何计算每一栏总的高度,将小的卡片放到最小栏里边
  2. 如何计算图片高度(不算难点)
实现

目前前端非常内卷,动不动就是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>
        </>
    );
};

结果展示:

image.png

建议后台直接返回图片资源的width、height这样省去了获取资源的时间,如果非要通过我们自己的方法获取资源图片的信息,前提资源图片不是很大,不然一次加载很多资源,每个资源挨个获取信息会导致等待时间过长

结尾

在此瀑布流js实现方式就已经完成了,代码中有一个获取图片资源的,最好是根据使用场景进行区分,因此那块没有加具体的实现,只是实现了在浏览器中,如果在小程序就用小程序获取图片资源的Api