1

需求

在业务中需要在web页面根据搜索的结果渲染出瀑布流样式的结果页面,我实现效果如下(感觉不太满意,他并不是通过计算最小高度然后自动填补,而是按照几列几行来一个个填补,后面有更深入的研究会更新,这是一种赶工实现的方式):
image.png

实现方法

尝试了直接使用flex布局,存在较大的缺陷,虽然看似实现了,但对于特别的图片显示还是很奇怪,不够贴合,后来查询了几种插件,不是说他们都不好,试下的效果应该是类似的,都是通过计算图形的宽高来进行布局的,不过有些对于数据中包含的信息要求比较高,让后台在我的数据里面塞图形宽高已经是我这个小前端的极限了!!最后找到了一个 react-grid-layout 插件,感谢大佬小翼回答中提供的几种插件。

代码

import React, { Component } from "react";
import styles from "./styles.module.less";
// 引入lodash
import _ from "lodash";
// 引入组件
import RGL, { WidthProvider } from "react-grid-layout";
// 包装组件
const ReactGridLayout = WidthProvider(RGL);
// 定义案例数组数据
const items = [
  {
    url:
      "https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=384124318,1246746555&fm=26&gp=0.jpg",
    width: 540,
    height: 300,
  },
  {
    url:
      "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2435568812,3959731342&fm=11&gp=0.jpg",
    width: 499,
    height: 320,
  },
  {
    url:
      "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3341500380,460051199&fm=11&gp=0.jpg",
    width: 604,
    height: 300,
  },
  {
    url:
      "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2435568812,3959731342&fm=11&gp=0.jpg",
    width: 499,
    height: 320,
  },
  {
    url:
      "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3341500380,460051199&fm=11&gp=0.jpg",
    width: 604,
    height: 300,
  },
  {
    url:
      "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3426709235,3578542279&fm=26&gp=0.jpg",
    width: 500,
    height: 210,
  },
  {
    url:
      "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1322120441,3602413475&fm=26&gp=0.jpg",
    width: 400,
    height: 183,
  },

  {
    url:
      "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3426709235,3578542279&fm=26&gp=0.jpg",
    width: 500,
    height: 210,
  },
  {
    url:
      "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3341500380,460051199&fm=11&gp=0.jpg",
    width: 604,
    height: 300,
  },
  {
    url:
      "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1322120441,3602413475&fm=26&gp=0.jpg",
    width: 400,
    height: 183,
  },
  {
    url:
      "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fi2.hdslb.com%2Fbfs%2Farchive%2F3c7a5b24b38ac3216182e0bf026622801d10c3fa.jpg&refer=http%3A%2F%2Fi2.hdslb.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1612319581&t=9486804e3256336fd0d2c2865d929d6c",
    width: 1728,
    height: 1080,
  },
  {
    url:
      "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic.51yuansu.com%2Fpic3%2Fcover%2F02%2F85%2F29%2F5a61c19d6a659_610.jpg&refer=http%3A%2F%2Fpic.51yuansu.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1612319668&t=aaa48c2a20b18f021f8058d7bd882dfb",
    width: 610,
    height: 1295,
  },
];

// 定义一个父组件,主要是为了进行容器大小的变化监听,然后动态设置高度
export default class About extends Component {
  constructor(props) {
    super(props);
    // 行高初始化,这是我自己的行高,根据当前容器大小计算出来的
    this.state = { rowHeight: 110 };
  }
  componentDidMount() {
    // 初始化size监听函数
    this.screenChange();
  }
  // 卸载size监听函数
  componentWillUnmount() {
    window.removeEventListener("resize", this.resize);
  }
  // size监听函数
  screenChange = () => {
    window.addEventListener("resize", this.resize);
  };

  // 区域宽高变化时触发行高的变化
  resize = () => {
    const box = document.getElementById("watar-fall");
    let width = box.clientWidth;
    let rowHeight = (width - 100) / 12;
    this.setState({ rowHeight });
    console.log(width);
  };

  render() {
    // 将行高作为props传入子组件
    const { rowHeight } = this.state;
    return (
      <div className={styles.bside_main_container} id="watar-fall">
        <WaterFall rowHeight={rowHeight} items={items}></WaterFall>
      </div>
    );
  }
}

// 定义的瀑布流子组件,真是应用可以单独封装在外部
class WaterFall extends Component {
  // 默认的props
  static defaultProps = {
    className: "layout",
    isDraggable: false,
    isResizable: false,
    cols: 12,
    rowHeight: 110,
  };
  constructor(props) {
    super(props);
    // 初始化定义layout 和渲染的数据
    this.state = { layout: this.generateLayout([]), data: [] };
  }

  // 加载的时候更新layout和data
  componentDidMount() {
    this.setState({
      data: [...this.props.items],
      layout: this.generateLayout(this.props.items),
    });
  }
  // 生成每一个瀑布流内item 的函数
  generateDOM = () => {
    const { data } = this.state;
    return _.map(_.range(data.length), function (i) {
      return (
        // 这里的key值和layout里面的要对应,所以不要用其他值代替
        <div key={i} className={styles.box}>
          {/*在这里可以自定义你要的显示样式 */}
          <img src={data[i].url} alt="ff" style={{ width: "100%" }}></img>
        </div>
      );
    });
  };

  // 生成layout的数组函数
  generateLayout = (items) => {
    // 循环数据返回相应的layout
    return _.map(new Array(items.length), function (item, i) {
      // 在这里设置每一个图片的高度。我是通过图片的宽然后根据所占行的比例,计算出来的等比高度
      let y = (3 / items[i].width) * items[i].height;
      return {
        x: (i * 3) % 12, // 每个图片的起始x点 ,因为是4列,所以在12列里面是3列一个图片
        y: Math.floor(i / 4) * y, // 间隔四个换行
        w: 3, // 图片占的列数
        h: y, // 计算所得的图片高度
        i: i.toString(), // layout的key值
      };
    });
  };

  render() {
    return (
      <div className={styles.container}>
        {/* 传入参数并生成瀑布流 */}
        <ReactGridLayout layout={this.state.layout} {...this.props}>
          {this.generateDOM()}
        </ReactGridLayout>
      </div>
    );
  }
}

总结

这次基本的实现是这样,这个组件本来更强大的地方应该是在可拖拽的方面,我这算是因小失大了,后面如果有时间再研究一下其他瀑布流或者自己写一个js组件吧。(如果我写的有什么问题,欢迎指正!)

新增改进

后来又稍微改进了一下,在方法二中,使用了计算最小高度值来进行布局分配。
直接放代码,已经封装成组件了,具体的调用方式如下:

<div className={styles.water_fall_box}>
    <WaterFall items={imgList} Child={Child} />
</div>

// 其中items 是需要展示的瀑布流数据,Child是每一个瀑布流卡片的渲染格式

const Child = item => {
  return (
    <div className={styles.preview_item}>
       {/*这是我自己封装的预览组件,可以不要 */}
      <PreviewDoc item={item} key={newUrl}>
        <div className={styles.pic_all}>
          <img src={item.url} alt='ff' style={{ width: '100%', height: '100%' }} className={styles.box}></img>
          <div
            className={styles.pic_title}
            onClick={e => {
              e.stopPropagation()
              // 你自己的函数,或者其他
            }}
          >
            <p className={styles.pic_name}>{item.title + '--' + item.fileName}</p>
          </div>
        </div>
      </PreviewDoc>
    </div>
  )
}

WaterFall 组件

/**
 * @desc 用于瀑布流渲染的组件
 * @name WaterFall
 * @author Cyearn
 * @date 2021/01/06
 */

import React, { Component } from 'react'
import styles from './styles.module.less'

export default class Home extends Component {
  static defaultProps = {
    items: [],
    gutter: 10,
    cols: 12,
    colWidth: 3,
    Child: () => {},
  }
  constructor(props) {
    super(props)
    this.state = {
      width: 1440, // 默认容器大小
      layout: [], // 初始化布局
      max: 10, // 瀑布流的最大高度
    }
  }

  componentDidMount() {
    // 初始化size监听函数
    this.screenChange()
    this.calculateLayout()
  }
  componentDidUpdate = preProps => {
    //  侦听数据变化
    if (JSON.stringify(this.props.items) !== JSON.stringify(preProps.items)) {
      this.calculateLayout()
    }
  }
  // 卸载size监听函数
  componentWillUnmount() {
    window.removeEventListener('resize', this.resize)
  }
  // size监听函数
  screenChange = () => {
    window.addEventListener('resize', this.resize)
  }

  // 生成瀑布流的item
  createDom = () => {
    const { layout } = this.state
    const { gutter, items, Child } = this.props
    console.log(items)
    return items.length > 0
      ? layout.map((v, i) => {
          return (
            <div
              className={styles.item_fall}
              style={{
                top: v.y,
                left: v.x,
                height: v.h,
                width: v.w,
                margin: `0 ${gutter}px`,
              }}
              key={v.i}
            >
              {items[i] && Child(items[i])}
            </div>
          )
        })
      : null
  }
  // 计算layout
  calculateLayout = () => {
    const { width } = this.state
    const { cols, colWidth, gutter, items } = this.props
    const count = cols / colWidth //总共有几列
    const harr = new Array(count).fill(gutter) // 初始化一个列数的全0数组

    let layout = [] // 定义一个空的布局列表
    let max = 10

    for (let i = 0; i < items.length; i++) {
      let min = Math.min.apply(null, harr) // 获取最短的列长

      let index = harr.indexOf(min) // 获取最短列的下标
      let oldHeight = harr[index] // 保存最短列长的原始高度
      let realWidth = (width - (count + 1) * 10) / count // 计算在dom中的宽度
      let realHeight = (items[i].height * realWidth) / items[i].width // 计算在dom中的高度
      harr[index] = harr[index] + realHeight + gutter // 给最短列加上当前item的高度
      max = Math.max.apply(null, harr) // 获取最短的列长
      let element = {
        x: index * (realWidth + gutter), // 绝对定位的left
        y: oldHeight, // 绝对定位的top
        w: realWidth, // 内容的宽
        h: realHeight, // 内容的高
        i: i.toString(), // 内容的key值
      }
      layout.push(element)
    }
    this.setState({ layout, max })
  }

  // 区域宽高变化时触发行高的变化
  resize = () => {
    const box = document.getElementById('watar_fall')
    let width = box.clientWidth
    // 监听窗口变化并重新计算布局
    this.setState({ width: width }, this.calculateLayout)
  }

  render() {
    const { max } = this.state
    return (
      <div className={styles.container} id='watar_fall' style={{ height: max }}>
        {this.createDom()}
      </div>
    )
  }
}

Cyearn
64 声望2 粉丝