CSS问题:如何实现瀑布流布局?

前端功能问题系列文章,点击上方合集↑

序言

大家好,我是大澈!

本文约2500+字,整篇阅读大约需要4分钟。

本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。

感谢关注微信公众号:"程序员大澈",然后加入问答群,从此让解决问题的你不再孤单!

1. 需求分析

在电商前台项目中,使用瀑布流的布局形式,展示商品图片列表。

让用户在浏览商品列表时,总有商品图片看不全,使用户可以无限滚动地查看列表下面的其它商品内容。

进而提高用户浏览量,增加商品曝光度,最终提升用户购买几率。

2. 实现步骤

2.1 什么是瀑布流布局呢

瀑布流布局,是页面上是一种参差不齐的多栏布局。

随着页面滚动条向下滚动,这种布局会不断加载新的数据内容,并附加至当前高度最低列的尾部。

它的特点是:布局宽度一致,高度不一致,上下错落排列。一般用于图片内容的展示。

2.2 代码实现

模版代码:

一行要展示几条数据,就定义几个column元素。这里以三列为例。

ini 复制代码
<template>
  <div class="page-main">
    <div class="card">
      <div class="coloum1">
        <div
          class="card-item"
          v-for="(item, index) in cardList1"
          :key="index"
          :style="[
            { background: item.color },
            { height: item.height },
            { lineHeight: item.height },
          ]"
          :class="{ visibles: isVisibility }"
        >
          <p class="text">{{ item.num }}</p>
        </div>
      </div>
      <div class="coloum2">
        <div
          class="card-item"
          v-for="(item, index) in cardList2"
          :key="index"
          :style="[
            { background: item.color },
            { height: item.height },
            { lineHeight: item.height },
          ]"
          :class="{ visibles: isVisibility }"
        >
          <p class="text">{{ item.num }}</p>
        </div>
      </div>
      <div class="coloum3">
        <div
          class="card-item"
          v-for="(item, index) in cardList3"
          :key="index"
          :style="[
            { background: item.color },
            { height: item.height },
            { lineHeight: item.height },
          ]"
          :class="{ visibles: isVisibility }"
        >
          <p class="text">{{ item.num }}</p>
        </div>
      </div>
    </div>
  </div>
</template>

逻辑代码:

第一次渲染时,先把已有数据按顺序正常展示。

然后利用nextTick钩子,在第二次渲染时,先获取所有元素,再循环遍历所有元素,再从第二行第一个元素开始,计算每一列高度和的最小值,把新数据放到最小高度列的数组数据中。以此类推,判断完所有已获取元素。

这里在两次渲染之间,可能会出现页面闪烁现象,所以做了元素显示隐藏的样式处理。

再就是,有多少列,则定义多少新的空数组。这里以三列为例。

ini 复制代码
<script setup>
import { ref, onMounted, reactive, nextTick } from "vue";

// 展示数据
const cardList = reactive([
  {
    num: "1号 100px",
    color: "#3498db",
    height: "100px",
  },
  {
    num: "2号 200px",
    color: "#2ecc71",
    height: "200px",
  },
  {
    num: "3号 60px",
    color: "#27ae60",
    height: "60px",
  },
  {
    num: "4号 80px",
    color: "#e67e22",
    height: "80px",
  },
  {
    num: "5号 60px",
    color: "#e74c3c",
    height: "60px",
  },
  {
    num: "6号 200px",
    color: "#7f8c8d",
    height: "200px",
  },
]);

// 由于渲染时候对数据的两次赋值,则会出现一次闪烁,需要做显示隐藏处理
const isVisibility = ref(true);

onMounted(() => {
  // 第一次渲染赋值
  equallyCard();

  // 第二次渲染赋值
  nextTick(() => {
    caLFlex();
  })
  .then(() => {
    // 闪烁显示隐藏处理
    isVisibility.value = true;
  });
});

// 各列的展示数据
const cardList1 = ref([]);
const cardList2 = ref([]);
const cardList3 = ref([]);

// 第一次渲染赋值
function equallyCard() {
  // 平分3列的数据,确保页面上遍历卡片dom的真实顺序与平分的一致
  let num = parseInt(cardList.length / 3);

  cardList.forEach((item, index) => {
    if (index < num) {
      cardList1.value.push(item);
      return;
    }
    if (index < 2 * num) {
      cardList2.value.push(item);
      return;
    }
    cardList3.value.push(item);
  });
}

// 第二次渲染赋值
function caLFlex() {
  let arr1 = []; // 第一列的新值
  let arr2 = []; // 第二列的新值
  let arr3 = []; // 第三列的新值
  let heightArry_1 = []; // 第一列的卡片高度
  let heightArry_2 = []; // 第二列的卡片高度
  let heightArry_3 = []; // 第三列的卡片高度

  Array.from(document.querySelectorAll(".card-item")).forEach((item, index) => {
    // 第一行中的元素无需判断,直接加到新数组中
    if (index === 0) {
      heightArry_1.push(item.offsetHeight);
      arr1.push(cardList[index]);
      return;
    }
    if (index === 1) {
      heightArry_2.push(item.offsetHeight);
      arr2.push(cardList[index]);
      return;
    }
    if (index === 2) {
      heightArry_3.push(item.offsetHeight);
      arr3.push(cardList[index]);
      return;
    }


    // 计算每一列高度
    const heightTotal_1 = heightArry_1.length
      ? Array.from(heightArry_1).reduce((accumulator, currentValue) => accumulator + currentValue)
      : 0; // 第一列的总高度
    const heightTotal_2 = heightArry_2.length
      ? Array.from(heightArry_2).reduce((accumulator, currentValue) => accumulator + currentValue)
      : 0; // 第二列的总高度
    const heightTotal_3 = heightArry_3.length
      ? Array.from(heightArry_3).reduce((accumulator, currentValue) => accumulator + currentValue)
      : 0; // 第三列的总高度


    // 找到高度最小值,并在最小高度新数组中添加新数据
    let minNumber = Math.min(heightTotal_1, heightTotal_2, heightTotal_3);
    switch (minNumber) {
      case heightTotal_1:
        heightArry_1.push(item.offsetHeight);
        arr1.push(cardList[index]);
        break;
      case heightTotal_2:
        heightArry_2.push(item.offsetHeight);
        arr2.push(cardList[index]);
        break;
      case heightTotal_3:
        heightArry_3.push(item.offsetHeight);
        arr3.push(cardList[index]);
        break;
    }
  });

  // 重新将数据赋值给各列数组
  cardList1.value = arr1;
  cardList2.value = arr2;
  cardList3.value = arr3;
}
</script>

样式代码:

使用了flex布局来做行的排版。这里根据个人项目实际需求自定义即可。

xml 复制代码
<style lang="scss" scoped>
.page-main {
  background: #ffffff;
  height: 100vh;
  overflow: hidden;
  padding: 0 30px;
  .card {
    display: flex;
    flex-direction: row;
    justify-content: space-around;
    .card-item {
      visibility: hidden;
      margin-bottom: 20px;
      text-align: center;
      width: 216px;
      border-radius: 16px;
    }
    .visibles {
      visibility: visible;
    }
  }
}
</style>

3. 问题详解

3.1 关于NextTick的个人拙见

作用:等待DOM更新后,再执行内部传入的回调函数

使用场景:

  • created中想要获取DOM
  • 响应式数据变化后获取DOM更新后的状态,如 获取列表更新后的高度

原理: 把nextTick回调方法放在renderWatcher回调之后执行,这样就能拿到更新后的DOM

3.2 瀑布流其它实现方式

关于瀑布流的实现方式,网上真的是五花八门,各种方法都有。但因为精力有限,其它方式我也没有再去尝试,只挑选了这么一种比较常用的实现方式,也就是flex布局+js动态计算列高度的方式。我觉的这种方式就足够了,尝试用着还不错。当然,本次实现的代码,不会是大澈个人空想而来,一定是站在了某位大佬的肩膀之上,又加上了一些个人的理解和拙见,才分享给了朋友们。

最后,也放上参考大佬的文章地址,大佬各种实现方式讲的挺全的,供大家参考:http://d5rhe.jbdi.cn/7b

结语

建立这个平台的初衷:

  • 打造一个仅包含前端问题的问答平台,让大家高效搜索处理同样问题。
  • 通过不断积累问题,一起练习逻辑思维,并顺便学习相关的知识点。
  • 遇到难题,遇到有共鸣的问题,一起讨论,一起沉淀,一起成长。

感谢关注微信公众号:"程序员大澈",然后加入问答群,从此让解决问题的你不再孤单!

相关推荐
小远yyds20 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
程序媛小果39 分钟前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
小光学长43 分钟前
基于vue框架的的流浪宠物救助系统25128(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。
数据库·vue.js·宠物
阿伟来咯~1 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端1 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱1 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai1 小时前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨1 小时前
在JS中, 0 == [0] 吗
开发语言·javascript
王哲晓3 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
理想不理想v3 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试