产品经理:下班之前,实现一个瀑布流

1. 瀑布流是什么

瀑布流, 又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为宽度相等高度不定的元素组成的参差不齐的多栏布局,随着页面向下滚动,新的元素附加到最短的一列而不断向下加载。

2. 瀑布流的实现原理

寻找各列之中高度最小者,并将新的元素添加到该列上,然后继续寻找所有列的高度最小者,继续添加到高度最小列上,一直到所有元素均按要求排列完成为止。

3. 瀑布流的使用场景

瀑布流滑动的时候会不停的出现新的东西,吸引你不断向下探索,巧妙的利用视觉层级,视线的任意流动来缓解视觉的疲劳,采用这种方案可以延长用户停留视觉,提高用户粘度,适合那些随意浏览,不带目的性的使用场景,就像逛街一样,边走边看,所以比较适合图片、商品、资讯类的场景,很多电商相关的网站都使用了瀑布流进行承载。

4. 瀑布流的的实现有哪些问题&如何解决

如何寻找所有列的高度最小者? 如何渲染瀑布流?

4.1 技术选型

采用 Vue 框架来实现瀑布流,其一些自带属性使我们的瀑布流实现更加简单。 通过 ref 可以很方便的获取每列高度。通过比较算法算出高度最小列。
拿到高度最小列之后,将下个要插入的元素数据放到最小列的数据列表中,通过操作数据完成元素渲染。
通过watch监测元素渲染,判断是否继续进行渲染和请求更多元素数据。

4.2 如何寻找所有列的高度最小者

每一列都定义一个ref,通过ref获取当前列的高度,然后比较高度取到最小高度,再通过最小高度算出其对应的列数。

4.3 如何渲染瀑布流

瀑布流常用在无限下拉加载或者加载数据量很大,且包含很多图片元素的情景,所以通常不会一次性拿到所有数据,也不会一次性将拿到的数据全部渲染到页面上, 否则容易造成页面卡顿影响用户体验, 所以何时进行渲染、何时继续请求数据就很关键。

4.4 何时渲染

选择渲染的区域为滚动高度+可视区域高度的1.5倍,即可以防止用户滚动到底部的时候白屏,也可以防止渲染过多影响用户体验。如果:最小列的高度 - 滚 动高度 < 可视区域高*1.5 则继续渲染元素,否则不再继续渲染。

4.5 何时请求数据

当已渲染的元素+可视区域可以展示的预估元素个数大于已请求到的个数的时候才去继续请求更多数据,防止请求浪费。如果:已加载的元素个数 + 一屏可以展示的元素预估个数 > 所有请求拿到的元素个数 则触发下一次请求去获取更多数据。


理论存在,实践开始

产品经理:下班之前,实现一个瀑布流_第1张图片

5. 开始动手

5.1 基础的样式布局,使用子绝父相。

创建商品列表的基本 html 和 css,让 item 相对于 class 为 goods 的进行排序(相对布局)

<div class="goods goods-waterfall">
    <div
      class="goods-item goods-waterfall-item"
      v-for="(item, index) in goodsData"
      :key="index"
    >
      <img class="goods-item-img" :src="item.productImage" />
      <div class="goods-item-desc">
        <p class="goods-item-desc-name">
          {{item.presentation}}
        p>
        <div class="goods-item-desc-data">
          <p class="goods-item-desc-data-price">¥{{item.salePrice}}p>
          <p class="goods-item-desc-data-volume">销量:{{item.productNum}}p>
        div>
      div>
    div>
  div>
5.2 生成不同高度的图片去撑起不同高度得到不同高度的item
/**
 * 返回随机的图片高度
 */
imgHeight() {
  // 随机数 * (高度区间)+ 最低的图片高度
  const result = Math.floor(
    Math.random() * (this.MAX_IMG_HEIGHT - this.MIN_IMG_HEIGHT)
      + this.MIN_IMG_HEIGHT,
  );
  return result;
},
/**
 * 根据随机高度,生成图片样式数据
 */
initImgStyles() {
  this.goodsData.forEach((item) => {
    this.imgStyles.push({
      height: `${this.imgHeight()}px`,
    });
  });
},

将随机生成的样式添加到 img 标签上

<img class="goods-item-img" :src="item.productImage" alt srcset :style="imgStyles[index]" />
5.3 计算 item 的位置,来达到 从上到下,从左到右依次排序的目的

瀑布流布局

  1. 获取到所有的 item 元素

  2. 遍历 item 元素,得到每一个 item 的高度,加上一个 margin 的高度

  3. 创建两个变量:leftHeightTotalrightHeightTotal,分别表示左右两侧目前距离顶部的高度,通过对比左右两侧距离顶部的高度,来确定 item 的放置位置。

​ 3.1 如果左侧小于等于右侧高度的话,(leftHeightTotal <= rightHeightTotal),那么 item 应该放置在左侧。此时 item 距离左侧为 0,距离顶部为当前的 leftHeightTotal

​ 3.2 否则,item 放置到右侧,此时 item 距离右侧为 0,距离顶部为当前的 rightHeightTotal

  1. 保存计算出的 item 的所有样式,配置到 item 上。

  2. item 配置完成之后,对比左右两侧最大的高度,最大的高度为goods 组件的高度

initWaterfall() {
  const $goodsItems = this.$refs.goodsItem;
  if (!$goodsItems) return;
  let leftHeightTotal = 0;
  let rightHeightTotal = 0;
  // 遍历 item 元素,得到每一个 item 的高度,加上一个 margin 的高度
  $goodsItems.forEach(($el, index) => {
    let goodsItemStyle = {};
    const elHeight = $el.clientHeight + this.itemMarginBottomSize;
    // 如果左侧小于等于右侧高度的话
    if (leftHeightTotal <= rightHeightTotal) {
      goodsItemStyle = {
        left: '0px',
        top: `${leftHeightTotal}px`,
      };
      // 更新距离顶部的高度
      leftHeightTotal += elHeight;
    } else {
      goodsItemStyle = {
        right: '0px',
        top: `${rightHeightTotal}px`,
      };
      rightHeightTotal += elHeight;
    }
    // 保存计算出的 item 样式,动态添加到每一个 item 上面。
    this.goodsItemStyles.push(goodsItemStyle);
  });
  // 在不需要 goods 自滑动的时候,再去计算 goodsView 的高度
  if (!this.isScroll) {
    this.goodsViewHeight = (leftHeightTotal > rightHeightTotal
          ? leftHeightTotal
          : rightHeightTotal) + 'px'
  }
},

将生成的动态样式 goodsItemStyles 添加到外层的 div 上面。


可以将这个封装为一个组件,或者一个插件,数据由 props 传入。实现数据与布局解耦。

完整代码如下:

<template>
  <!--
    瀑布流布局的基本思路
    1. 创建商品列表的基本 html 和 css,让 item 相对于 class 为 goods 的进行排序(相对布局)
    2. 生成不同高度的图片去撑起不同高度得 item
    3. 计算 item 的位置,来达到 从上到下,从左到右依次排序的目的
  -->
  <div
    class="goods goods-waterfall"
    :style="{height: goodsViewHeight}"
    ref="goods"
  >
    <div
      class="goods-item goods-waterfall-item"
      :style="goodsItemStyles[index]"
      v-for="(item, index) in goodsData"
      :key="index"
      ref="goodsItem"
    >
    <!-- 给数据中的每一张图片添加随机高度的样式 -->
      <img class="goods-item-img" :src="item.productImage" alt srcset :style="imgStyles[index]" />
      <div class="goods-item-desc">
        <p class="goods-item-desc-name">
          {{item.presentation}}
        </p>
        <div class="goods-item-desc-data">
          <p class="goods-item-desc-data-price">{{item.salePrice}}</p>
          <p class="goods-item-desc-data-volume">销量:{{item.productNum}}</p>
        </div>
      </div>
    </div>
  </div>
</template>


<script>
import axios from 'axios';

export default {
  data() {
    return {
      // 最小高度
      MIN_IMG_HEIGHT: 178,
      // 最大高度
      MAX_IMG_HEIGHT: 230,
      goodsData: [],
      goodsViewHeight: '100%',
      itemMarginBottomSize: 8,
      // 图片样式集合
      imgStyles: [],
      goodsItemStyles: [],
      page: 1,
      pageSize: 100,
      sortFlag: true,
      priceLevel: this.priceChecked,
      priceChecked: 'all',
    };
  },
  created() {
    this.initLayout();
  },
  mounted() {
    this.initData();
  },
  methods: {
    initData() {
      const param = {
        page: this.page,
        pageSize: this.pageSize,
        sort: this.sortFlag ? 1 : -1,
        priceLevel: this.priceChecked,
      };
      axios.get('/api/goods/list', {
        params: param,
      }).then((res) => {
        this.goodsData = res.data.result.list;
        this.initLayout();
      });
    },
    /**
     * 设置布局
     */
    initLayout() {
      this.goodsViewHeight = '100%';
      this.goodsItemStyles = [];
      this.imgStyles = [];
      this.$nextTick(() => {
        this.initWaterfall();
      });
    },
    /**
     * 返回随机的图片高度
     */
    imgHeight() {
      // 随机数 * (高度区间)+ 最低的图片高度
      const result = Math.floor(
        Math.random() * (this.MAX_IMG_HEIGHT - this.MIN_IMG_HEIGHT)
          + this.MIN_IMG_HEIGHT,
      );
      return result;
    },
    /**
     * 根据随机高度,生成图片样式数据
     */
    initImgStyles() {
      this.goodsData.forEach((item) => {
        this.imgStyles.push({
          height: `${this.imgHeight()}px`,
        });
      });
    },
    /**
     * 瀑布流布局
     * 1. 获取到所有的 item 元素
     * 2. 遍历 item 元素,得到每一个 item 的高度,加上一个 margin 的高度
     * 3. 创建两个变量:leftHeightTotal,rightHeightTotal,分别表示左右两侧目前距离顶部的高度
     * 通过对比左右两侧距离顶部的高度,来确定 item 的放置位置。
     *  3.1 如果左侧小于等于右侧高度的话,(leftHeightTotal <= rightHeightTotal),那么 item 应该放置在左侧。此时 item 距离左侧为 0,距离顶部为当前的 leftHeightTotal
     *  3.2 否则,item 放置到右侧,此时 item 距离右侧为 0,距离顶部为当前的 rightHeightTotal。
     * 4. 保存计算出的 item 的所有样式,配置到 item 上。
     * 5. item 配置完成之后,对比左右两侧最大的高度,最大的高度为goods 组件的高度
     */
    initWaterfall() {
      const $goodsItems = this.$refs.goodsItem;
      if (!$goodsItems) return;
      let leftHeightTotal = 0;
      let rightHeightTotal = 0;
      // 遍历 item 元素,得到每一个 item 的高度,加上一个 margin 的高度
      $goodsItems.forEach(($el, index) => {
        let goodsItemStyle = {};
        const elHeight = $el.clientHeight + this.itemMarginBottomSize;
        // 如果左侧小于等于右侧高度的话
        if (leftHeightTotal <= rightHeightTotal) {
          goodsItemStyle = {
            left: '0px',
            top: `${leftHeightTotal}px`,
          };
          // 更新距离顶部的高度
          leftHeightTotal += elHeight;
        } else {
          goodsItemStyle = {
            right: '0px',
            top: `${rightHeightTotal}px`,
          };
          rightHeightTotal += elHeight;
        }
        // 保存计算出的 item 样式,动态添加到每一个 item 上面。
        this.goodsItemStyles.push(goodsItemStyle);
      });
      // 在不需要 goods 自滑动的时候,再去计算 goodsView 的高度
      if (!this.isScroll) {
        this.goodsViewHeight = (leftHeightTotal > rightHeightTotal
          ? leftHeightTotal
          : rightHeightTotal) + 'px'
      }
    },
  }
};
</script>


<style lang="scss" scoped>
@import "@css/style.scss";
.goods {
  background-color: $bgColor;
  &-scroll {
    overflow: hidden;
    overflow-y: auto;
  }
  &-item {
    background-color: white;
    padding: $marginSize;
    box-sizing: border-box;

    &-desc {
      width: 100%;
      &-name {
        // 文本最多两行,超出显示省略号
        font-size: $infoSize;
        overflow: hidden;
        text-overflow: ellipsis;
        word-break: break-word;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        display: -webkit-box;
        line-height: px2rem(18);

        &-hint {
          color: $textHintColor;
        }
      }

      &-data {
        width: 100%;
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin-top: $marginSize;
        &-price {
          font-size: $titleSize;
          color: $mainColor;
          font-weight: 500;
        }

        &-volume {
          font-size: $infoSize;
          color: $textHintColor;
        }
      }
    }
  }
}

// 瀑布流样式
.goods-waterfall {
  position: relative;
  margin: $marginSize;
  &-item {
    width: 49%;
    border-radius: $radiusSize;
    position: absolute;
    // 不指定高度,使用图片不同的高度来撑起盒子的高度
    .goods-item-img {
      width: 100%;
    }
  }
}
</style>


你以为这就结束了?少年,你太天真了。。。

产品经理:下班之前,实现一个瀑布流_第2张图片

来来来,我们接着学

产品经理:下班之前,实现一个瀑布流_第3张图片

6. 切换不同的展示形式(垂直布局,网格布局,瀑布流布局)

如何在同一组件中去展示不同的样式:

  1. html 表示整个布局的结构,具体的展示样式,将由不同的 css 决定

  2. 每种展示样式对应不同的 css,也就是对应不同的类名(css)
    ​ 1. 垂直列表 -> goods-list
    ​ 2. 网格布局 -> goods-grid
    ​ 3. 瀑布流布局 -> goods-waterfall

  3. 实现不同的展示形式,本质就是实现不同的 css 样式。

props: {
  /** 通过传入的参数,来决定展示什么样的形式
   * 1:列表布局
   * 2:网格布局
   * 3:瀑布流布局
   */
  layoutType: {
    type: String,
    default: '1',
  },
},

通过判断不同的 layoutType 决定使用的 class 类

initLayout() {
  this.goodsViewHeight = '100%';
  this.goodsItemStyles = [];
  this.imgStyles = [];
  switch (this.layoutType) {
    // 垂直列表
    case '1':
      (this.layoutClass = 'goods-list'),
      (this.layoutItemClass = 'goods-list-item');
      break;
    // 网格布局
    case '2':
      (this.layoutClass = 'goods-grid'),
      (this.layoutItemClass = 'goods-grid-item');
      break;
    // 瀑布流布局
    case '3':
      (this.layoutClass = 'goods-waterfall'),
      (this.layoutItemClass = 'goods-waterfall-item');
      this.initImgStyles();
      this.$nextTick(() => {
        this.initWaterfall();
      });
      break;
  }
},

为最外层组件添加动态类名

:class="[layoutClass, {'goods-scroll' : isScroll}]"

这样呢,我们的组件就大概完成了。把请求数据的方法放在父组件,实现解耦

<template>
  <!--
    瀑布流布局的基本思路
    1. 创建商品列表的基本 html 和 css,让 item 相对于 class 为 goods 的进行排序(相对布局)
    2. 生成不同高度的图片去撑起不同高度得 item
    3. 计算 item 的位置,来达到 从上到下,从左到右依次排序的目的

    如何在同一组件中去展示不同的样式:
    1. html 表示整个布局的结构,具体的展示样式,将由不同的 css 决定
    2. 每种展示样式对应不同的 css,也就是对应不同的类名(css)
      1. 垂直列表 -> goods-list
      2. 网格布局 -> goods-grid
      3. 瀑布流布局 -> goods-waterfall
    3. 实现不同的展示形式,本质就是实现不同的 css 样式。
  -->
  <div
    class="goods"
    :class="[layoutClass, {'goods-scroll' : isScroll}]"
    :style="{height: goodsViewHeight}"
    @scroll="onScrollChange"
    ref="goods"
  >
    <div
      class="goods-item"
      :class="layoutItemClass"
      :style="goodsItemStyles[index]"
      v-for="(item, index) in goodsData"
      :key="index"
      ref="goodsItem"
    >
    <!-- 给数据中的每一张图片添加随机高度的样式 -->
      <img class="goods-item-img" :src="item.productImage" alt srcset :style="imgStyles[index]" />
      <div class="goods-item-desc">
        <p class="goods-item-desc-name">
          <!-- <direct v-if="item.isDirect"></direct>
          <no-have v-if="item.isHave === '有货'"></no-have> -->
          {{item.presentation}}
        </p>
        <div class="goods-item-desc-data">
          <p class="goods-item-desc-data-price">{{item.salePrice}}</p>
          <p class="goods-item-desc-data-volume">销量:{{item.productNum}}</p>
        </div>
      </div>
    </div>
  </div>
</template>


<script>
export default {
  props: {
    /** 通过传入的参数,来决定展示什么样的形式
     * 1:列表布局
     * 2:网格布局
     * 3:瀑布流布局
     */
    layoutType: {
      type: String,
      default: '1',
    },
    /**
     * 是否允许 goods 单独滑动
     */
    isScroll: {
      type: Boolean,
      default: true,
    },
    goodsData: {
      type: Array,
      default: []
    }
  },
  data() {
    return {
      // 最小高度
      MIN_IMG_HEIGHT: 178,
      // 最大高度
      MAX_IMG_HEIGHT: 230,
      // goodsData: [],
      // 不同展示形式下的类名
      layoutClass: 'goods-list',
      layoutItemClass: 'goods-list-item',
      sortGoodsData: [],
      goodsViewHeight: '100%',
      itemMarginBottomSize: 8,
      // 图片样式集合
      imgStyles: [],
      goodsItemStyles: [],
      scrollTopValue: 0,
    };
  },
  created() {
    this.initLayout();
  },
  mounted() {
    this.initLayout();
  },
  activated() {
    /**
     * 定位页面滑动位置,需要配合keepAlive 来使用
     */
    this.$refs.goods.scrollTop = this.scrollTopValue;
  },
  methods: {
    /**
     * 设置布局,为不同的 layoutType 设定不同的展示形式
     * 1. 初始化影响布局的数据
     *    1. goodsViewHeight -> 在垂直布局,网格布局下都是 100%,瀑布流布局下为实际高度
     *    2. goodsItemStyles
     *    3. imgStyles
     * 2. 为不同的 layoutType 设置不同的展示类
     */
    initLayout() {
      this.goodsViewHeight = '100%';
      this.goodsItemStyles = [];
      this.imgStyles = [];
      switch (this.layoutType) {
        // 垂直列表
        case '1':
          (this.layoutClass = 'goods-list'),
          (this.layoutItemClass = 'goods-list-item');
          break;
        // 网格布局
        case '2':
          (this.layoutClass = 'goods-grid'),
          (this.layoutItemClass = 'goods-grid-item');
          break;
        // 瀑布流布局
        case '3':
          (this.layoutClass = 'goods-waterfall'),
          (this.layoutItemClass = 'goods-waterfall-item');
          this.initImgStyles();
          this.$nextTick(() => {
            this.initWaterfall();
          });
          break;
      }
    },
    /**
     * 返回随机的图片高度
     */
    imgHeight() {
      // 随机数 * (高度区间)+ 最低的图片高度
      const result = Math.floor(
        Math.random() * (this.MAX_IMG_HEIGHT - this.MIN_IMG_HEIGHT)
          + this.MIN_IMG_HEIGHT,
      );
      return result;
    },
    /**
     * 根据随机高度,生成图片样式数据
     */
    initImgStyles() {
      this.goodsData.forEach((item) => {
        this.imgStyles.push({
          height: `${this.imgHeight()}px`,
        });
      });
    },
    /**
     * 瀑布流布局
     * 1. 获取到所有的 item 元素
     * 2. 遍历 item 元素,得到每一个 item 的高度,加上一个 margin 的高度
     * 3. 创建两个变量:leftHeightTotal,rightHeightTotal,分别表示左右两侧目前距离顶部的高度
     * 通过对比左右两侧距离顶部的高度,来确定 item 的放置位置。
     *  3.1 如果左侧小于等于右侧高度的话,(leftHeightTotal <= rightHeightTotal),那么 item 应该放置在左侧。此时 item 距离左侧为 0,距离顶部为当前的 leftHeightTotal
     *  3.2 否则,item 放置到右侧,此时 item 距离右侧为 0,距离顶部为当前的 rightHeightTotal。
     * 4. 保存计算出的 item 的所有样式,配置到 item 上。
     * 5. item 配置完成之后,对比左右两侧最大的高度,最大的高度为goods 组件的高度
     */
    initWaterfall() {
      const $goodsItems = this.$refs.goodsItem;
      if (!$goodsItems) return;
      let leftHeightTotal = 0;
      let rightHeightTotal = 0;
      // 遍历 item 元素,得到每一个 item 的高度,加上一个 margin 的高度
      $goodsItems.forEach(($el, index) => {
        let goodsItemStyle = {};
        const elHeight = $el.clientHeight + this.itemMarginBottomSize;
        // 如果左侧小于等于右侧高度的话
        if (leftHeightTotal <= rightHeightTotal) {
          goodsItemStyle = {
            left: '0px',
            top: `${leftHeightTotal}px`,
          };
          // 更新距离顶部的高度
          leftHeightTotal += elHeight;
        } else {
          goodsItemStyle = {
            right: '0px',
            top: `${rightHeightTotal}px`,
          };
          rightHeightTotal += elHeight;
        }
        // 保存计算出的 item 样式,动态添加到每一个 item 上面。
        this.goodsItemStyles.push(goodsItemStyle);
      });
      // 在不需要 goods 自滑动的时候,再去计算 goodsView 的高度
      if (!this.isScroll) {
        this.goodsViewHeight = (leftHeightTotal > rightHeightTotal
          ? leftHeightTotal
          : rightHeightTotal) + 'px'
      }
    },
    /**
     * 滑动变化
     */
    onScrollChange($e) {
      this.scrollTopValue = $e.target.scrollTop;
    },
  },
  watch: {
    /**
     * 监听布局类型切换
     * 1:列表布局
     * 2:网格布局
     * 3:瀑布流布局
     */
    layoutType() {
      this.initLayout();
    },
  },
};
</script>


<style lang="scss" scoped>
@import "@css/style.scss";
.goods {
  background-color: $bgColor;
  &-scroll {
    overflow: hidden;
    overflow-y: auto;
  }
  &-item {
    background-color: white;
    padding: $marginSize;
    box-sizing: border-box;

    &-desc {
      width: 100%;
      &-name {
        // 文本最多两行,超出显示省略号
        font-size: $infoSize;
        overflow: hidden;
        text-overflow: ellipsis;
        word-break: break-word;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        display: -webkit-box;
        line-height: px2rem(18);

        &-hint {
          color: $textHintColor;
        }
      }

      &-data {
        width: 100%;
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin-top: $marginSize;
        &-price {
          font-size: $titleSize;
          color: $mainColor;
          font-weight: 500;
        }

        &-volume {
          font-size: $infoSize;
          color: $textHintColor;
        }
      }
    }
  }
}

.goods-list {
  &-item {
    display: flex;
    border-bottom: 1px solid $lineColor;
    .goods-item-img {
      width: px2rem(120);
      height: px2rem(120);
    }
    .goods-item-desc {
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      padding: $marginSize;
    }
  }
}

.goods-grid {
  margin: $marginSize;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  &-item {
    width: 49%;
    border-radius: $radiusSize;
    margin-bottom: $marginSize;
    .goods-item-img {
      width: 100%;
    }
  }
}

// 瀑布流样式
.goods-waterfall {
  position: relative;
  margin: $marginSize;
  &-item {
    width: 49%;
    border-radius: $radiusSize;
    position: absolute;
    // 不指定高度,使用图片不同的高度来撑起盒子的高度
    .goods-item-img {
      width: 100%;
    }
  }
}
</style>

产品经理:下班之前,实现一个瀑布流_第4张图片


好复杂啊,老子不想学了。改行改行,别急少年,老夫教你一个简单的,让你马上下班陪你女朋友

产品经理:下班之前,实现一个瀑布流_第5张图片

纯 CSS 实现瀑布流

基础的页面结构

DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
  <style>
    .masonry {
      width: 1440px;
      margin: 20px auto;
    }

    .item {
      width: 100%;
      margin-bottom: 30px;
    }

    .item img {
      width: 100%;
    }

    .item h2 {
      padding: 8px 0;
    }

    .item P {
      color: #555;
    }
  style>
head>

<body>
  <div class="masonry">
    <div class="item">
      <img src="http://source.unsplash.com/random/400x300" />
      <h2>Title Goes Hereh2>
      <p>
        Lorem, ipsum dolor sit amet consectetur adipisicing elit. Quis quod et
        deleniti nobis quasi ad, adipisci perferendis totam, ducimus incidunt
        dolore aut, quae quaerat architecto quisquam repudiandae amet nostrum
        quidem?
      p>
    div>
    <div class="item">
      <img src="http://source.unsplash.com/random/400x400" />
      <h2>Title Goes Hereh2>
      <p>
        Lorem, ipsum dolor sit amet consectetur adipisicing elit. Quis quod et
        deleniti nobis quasi ad, adipisci perferendis totam, ducimus incidunt
        dolore aut, quae quaerat architecto quisquam repudiandae amet nostrum
        quidem?
      p>
    div>
    <div class="item">
      <img src="http://source.unsplash.com/random/400x500" />
      <h2>Title Goes Hereh2>
      <p>
        Lorem, ipsum dolor sit amet consectetur adipisicing elit. Quis quod et
        deleniti nobis quasi ad, adipisci perferendis totam, ducimus incidunt
        dolore aut, quae quaerat architecto quisquam repudiandae amet nostrum
        quidem?
      p>
    div>
    <div class="item">
      <img src="http://source.unsplash.com/random/400x600" />
      <h2>Title Goes Hereh2>
      <p>
        Lorem, ipsum dolor sit amet consectetur adipisicing elit. Quis quod et
        deleniti nobis quasi ad, adipisci perferendis totam, ducimus incidunt
        dolore aut, quae quaerat architecto quisquam repudiandae amet nostrum
        quidem?
      p>
    div>
    <div class="item">
      <img src="http://source.unsplash.com/random/400x800" />
      <h2>Title Goes Hereh2>
      <p>
        Lorem, ipsum dolor sit amet consectetur adipisicing elit. Quis quod et
        deleniti nobis quasi ad, adipisci perferendis totam, ducimus incidunt
        dolore aut, quae quaerat architecto quisquam repudiandae amet nostrum
        quidem?
      p>
    div>
    <div class="item">
      <img src="http://source.unsplash.com/random/400x750" />
      <h2>Title Goes Hereh2>
      <p>
        Lorem, ipsum dolor sit amet consectetur adipisicing elit. Quis quod et
        deleniti nobis quasi ad, adipisci perferendis totam, ducimus incidunt
        dolore aut, quae quaerat architecto quisquam repudiandae amet nostrum
        quidem?
      p>
    div>
    <div class="item">
      <img src="http://source.unsplash.com/random/400x600" />
      <h2>Title Goes Hereh2>
      <p>
        Lorem, ipsum dolor sit amet consectetur adipisicing elit. Quis quod et
        deleniti nobis quasi ad, adipisci perferendis totam, ducimus incidunt
        dolore aut, quae quaerat architecto quisquam repudiandae amet nostrum
        quidem?
      p>
    div>
    <div class="item">
      <img src="http://source.unsplash.com/random/400x600" />
      <h2>Title Goes Hereh2>
      <p>
        Lorem, ipsum dolor sit amet consectetur adipisicing elit. Quis quod et
        deleniti nobis quasi ad, adipisci perferendis totam, ducimus incidunt
        dolore aut, quae quaerat architecto quisquam repudiandae amet nostrum
        quidem?
      p>
    div>
  div>
  <script>

  script>
body>

html>

非常简单的结构
加上 columns 属性,立马变得不一样

.masonry {
      width: 1440px;
      margin: 20px auto;
      columns: 4;
      column-gap: 30px;
}

.item {
      width: 100%;
      break-inside: avoid;
      margin-bottom: 30px;
}


是不是相当简单嘞

赶快写完,约会去吧


参考文章:https://segmentfault.com/a/1190000023103516

你可能感兴趣的:(vue,vue.js,css,html5)