大厂在用的css+js实现不等高瀑布流布局

前言

  • 常网IT源码上线啦!
  • 本篇录入技术选型专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。
  • 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。
  • 接下来想分享一些自己在项目中遇到的技术选型以及问题场景。

我们的经济是一片大海,而不是一个小池塘,大海啊,有风平浪静之时,也有疯狂暴雨之时,没有疯狂暴雨,那就不是大海了。狂风骤雨可以掀翻小。但不能掀翻大海。经历了无数次狂风骤雨,大海依旧在那儿,经历了5000多年的艰难困苦,依旧在那儿。面向未来,将永远在这儿。


一、前言

页面中显示四列卡片,每一个卡片的高度根据内容决定(即:高度不相同),宽度相同。

期望效果:如同小红书的瀑布流形式排列。

元素以不规则的方式排列,就像瀑布中的流水一样,每个元素的高度可以不同。

二、分析

现在使用的技术栈是flex布局,正如图中所示,第一个卡片的内容较少,第二个最长。

第五个为第二行,现在被排列的位置,期望排列在第一个的后面。

如图所示:(期望效果)

三、相对re+绝对ab

我现在的做法:相对re+绝对ab

因为我是一行四列,宽度设置了23%。用手动定位的形式,控制left和top设置距离位置。

java 复制代码
<div class="body">
  <div class="body-item" v-for="(card, cardIndex) in cardList"  
      :style="{ left: left(cardIndex) }"></div>
</div>

<style scoped lang='scss'>
.body {
  width: 100%;
  // 本来用的flex布局,但会出现图一的情况
  //   display: flex;
  //   flex-wrap: wrap;
  gap: 30px;
  position: relative;
  // height: 3000px; 不能写死
  &-item {
    width: 23%;
    position: absolute;
  }
}
</style

设置每个元素的left,可以用计算属性

解释:

第一个元素的left:0

第二个元素:23%(元素本身的宽度) + 2%(这是元素与元素之前的边距)

第三个元素:23% + 2% + 23% + 2% = 50%

第四个:23% + 2% + 23% + 2% + 23% + 1% = 74%

baseValues这个就是我们的边距

groupOffset:前面元素的宽度

java 复制代码
computed: {
  left() {
      return (index) => {
        const groupOffset = (index % 4) * 23
        const baseValues = [1, 2, 4, 5]
        const base = groupOffset ? baseValues[index % 4] : 0 // 循环取基础值
        return index ? `${groupOffset + base}%` : 0
      }
    },
}

效果

left搞定,剩下top

top本来也是想和top绑定在元素上面,动态属性计算,但会动态计算出元素的高度,发现不实时。

offsetHeight:取的值不是很准确

而且我还要读取上一次元素的高度,所以就不在计算属性这里写了。

java 复制代码
top(){
  this.$nextTick(() => {
    return (index) => {
      const item = document.querySelectorAll('.Visualization-item')
      return item[index].offsetHeight
    }
  }
}

解释:

第一行的top我们都设为0

group :Math.floor(index / 4):用于计算元素所在的行号,知道行号,就知道取第几行的元素了

java 复制代码
// 假设元素索引为:
// 第0行:0,1,2,3
// 第1行:4,5,6,7
// 第2行:8,9,10,11...

Math.floor(0/4) = 0   // 第0行
Math.floor(5/4) = 1   // 第1行
Math.floor(8/4) = 2   // 第2行

循环内,主要是计算:假设要计算第6个元素,那top等于 =

  • 我要先找他是第几行,(第2行)

  • 第1行的第2个元素的高度

如果第10个,top等于

  • 他是第3行

  • 第1行的第2个元素的高度 + 第2行的第2个元素(6)的高度

所以可以看到,我们会用到累加高度

java 复制代码
mounted() {
  // 循环累加前序行中同列元素的高度和间隔
  // 最终高度 = 所有前序行同列元素高度之和 + 间隔总和
  document.querySelectorAll('.Visualization-item').forEach((f, index) => {
      this.$nextTick(() => {
        //    0 1 2 3
        //    4 5 6 7
        //    8 9 10 

        const baseIndex = index % 4 // 基础索引 0-3
        const group = Math.floor(index / 4) // 行号

        // 计算累计高度:前序行高度之和 + 间隔
        let totalHeight = 0
        for (let i = 0; i < group; i++) {
          const prevIndex = baseIndex + i * 4
          totalHeight += this.getFirstItemHeight(prevIndex) + 20
        }
        f.style.top = `${totalHeight}px`
      })
    })
}

我们用定位相对re+绝对ab,就要给父元素设置高度,高度要自动计算

java 复制代码
methods: {
  setEleHeight() {
    const container = document.querySelector('.Visualization');
    let maxBottom = 0;

    document.querySelectorAll('.Visualization-item').forEach((f, index) => {
      this.$nextTick(() => {
        // ... 原有定位逻辑 ...
        f.style.top = `${totalHeight}px`

        // 新增高度计算
        const itemBottom = totalHeight + f.offsetHeight;
        if (itemBottom > maxBottom) {
          maxBottom = itemBottom;
        }
        container.style.height = `${maxBottom + 50}px`; // 增加50px缓冲
      })
    })
  }
}

样式部分需要移除固定高度:

java 复制代码
.Visualization {
  // 注释掉固定高度
  // height: 3000px;
}

完美了

四、优化

你可是高级前端开发,我这里的瀑布流用的是动态设置left和top,会不会影响性能呢?第一反应要想到这,改成transform是不是比较好?

那肯定。

其实我是参考小红书的,平时娱乐的时候也要思考技术栈

java 复制代码
// 传统定位 (触发回流)
element.style.left = '100px'; 

// transform 定位 (只触发重绘)
element.style.transform = 'translateX(100px)';
  • GPU 加速渲染

  • 60fps 流畅动画

  • 最小的回流触发

当元素需要频繁更新位置时,这种实现方式比直接操作 left/top 属性性能提升约 30%-50%。

八股文背归背,要实际用上。

开搞。

java 复制代码
<div class="Visualization">
  <div class="Visualization-item" v-for="(card, cardIndex) in cardList"></div>
</div>

<style scoped lang='scss'>
.Visualization {
  width: 100%;
  // 本来用的flex布局,但会出现图一的情况
  //   display: flex;
  //   flex-wrap: wrap;
  gap: 30px;
  position: relative;
  &-item {
    width: 23%;
    position: absolute;
    left: 0;
    top: 0;
    // 添加GPU加速
  will-change: transform;
  // 优化过渡效果
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);  /* 浏览器会使用独立的合成层渲染 */
  }
}
</style

js处理

transform:translate的translateX就是我们的left,y轴就是我们的top

  • 而top还是之前的逻辑

  • left不能写百分比了,我们可以巧妙的将百分比转换为精确像素值(父元素的宽度 * 百分比) / 100即可

生怕大家看不太懂,所以注释加得比较多~

java 复制代码
setEleHeight() {
      const container = document.querySelector('.Visualization')
      let maxBottom = 0

      document.querySelectorAll('.Visualization-item').forEach((f, index) => {
        this.$nextTick(() => {
          const baseIndex = index % 4 // 基础索引 0-3
          const group = Math.floor(index / 4) // 行号

          // 计算累计高度:前序行高度之和 + 间隔
          let totalHeight = 0
          for (let i = 0; i < group; i++) {
            const prevIndex = baseIndex + i * 4
            totalHeight += this.getFirstItemHeight(prevIndex) + 20
          }

          // 计算横向偏移(将百分比转换为像素)
          const containerWidth = f.parentElement.offsetWidth // 父容器宽度
          const groupOffset = (index % 4) * 23 // 百分比偏移
          const baseValues = [1, 2, 4, 5]
          const base = groupOffset ? baseValues[index % 4] : 0
          const translateX = (containerWidth * (groupOffset + base)) / 100 // 将百分比转换为精确像素值

          //   f.style.top = `${totalHeight}px`
          f.style.transform = `translate(${translateX}px, ${totalHeight}px)`

          // 新增高度计算
          const itemBottom = totalHeight + f.offsetHeight // 之前的高度 + 自己的高度
          if (itemBottom > maxBottom) {
            maxBottom = itemBottom
          }
          container.style.height = `${maxBottom + 50}px` // 增加50px缓冲
        })
      })
    },

效果

那我们的left是px,如果窗口大小改变,要重新计算一下,还要考虑防抖

java 复制代码
methods:{
  // 新增防抖函数
    debounce(fn, delay) {
      let timeoutId
      return (...args) => {
        clearTimeout(timeoutId)
        timeoutId = setTimeout(() => fn.apply(this, args), delay)
      }
    },
    handleResize() {
      this.debounce(this.setEleHeight, 200)()
    },
},
mounted() {
    this.setEleHeight()
    window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
    window.removeEventListener('resize', this.handleResize)
},

这回牛了!

五、考虑封装成组件化

子组件-瀑布流组件WaterfallFlow.vue

默认四列

this.$slots.default:我们获取子组件通过$slots插槽,需要注意的是子组件需要加ref(v-for也得加,不怕重复,因为当使用 v-for 时 ref 会被收集为数组)

本来想通过document.querySelectorAll 获取,但获取不到子组件slot的元素:

  1. 作用域隔离:父组件传递的 class 会被 Vue 的 scoped CSS 自动添加哈希后缀

  2. 渲染时序:slot 内容渲染时机晚于子组件 mounted 钩子

  3. DOM 位置:slot 内容实际存在于父组件的 DOM 树中

组件代码移步:传送门

使用的组件

java 复制代码
  <WaterfallFlow class="Visualization" childrenClass="Visualization-item" :columnCount="4" :gap="20" :itemWidth="23">
    <div class="Visualization-item" v-for="(card, cardIndex) in cardList" ref="flowItems" :key="cardIndex">

    </div>
  </WaterfallFlow>  


<style scoped lang='scss'>
.Visualization {
  gap: 30px;
  &-item {
    // 子组件写
    width: 23%;
    // height: 100%;
    position: absolute;
    left: 0;
    top: 0;
    // 添加GPU加速
    will-change: transform;
    // 优化过渡效果
    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }
}
</style>

六、现有技术方案

当初要实现这样的布局都是依赖于JavaScript来实现,所以当时出现过很多实现瀑布流布局的插件。比如MasonryIsotope等都是非常有名的插件。

有人尝试通过Multi-columns相关的属性column-countcolumn-gap配合break-inside来实现瀑布流布局

七、瀑布流布局的性能优化

减少重排和重绘

  • 使用 transformopacity 属性来实现动画效果。
  • 减少对 DOM 的操作,例如 DocumentFragment

图片优化

  • 使用WebP,它通常比 JPEG 或 PNG 格式更小,但保持相同的质量。

  • 实施图片懒加载。

  • 为图片设置宽度和高度属性,以避免页面在加载图片时发生布局变化。

使用虚拟化或分页

当瀑布流中的项目数量非常多时,渲染所有项目可能会对性能产生影响。

我是掘金Dignity_呱,至此撒花~

后记

希望本文对你有点帮助,或者引起思考。

我们在实际项目中或多或少遇到一些奇奇怪怪的问题。

自己也会对一些写法的思考,为什么不行🤔,又为什么行了?

最后,祝君能拿下满意的offer。

我是Dignity_呱,来交个朋友呀,有朋自远方来,不亦乐乎呀!深夜末班车

👍 如果对您有帮助,您的点赞是我前进的润滑剂。

以往推荐

前端哪有什么设计模式(12k+)

为什么没人用mixin(7k+)

小小导出,我大前端足矣!

靓仔,说一下keep-alive缓存组件后怎么更新及原理?

面试官问我new Vue阶段做了什么?

前端仔,快把dist部署到Nginx上

多图详解,一次性啃懂原型链(上万字)

Vue-Cli3搭建组件库

Vue实现动态路由(和面试官吹项目亮点)

VuePress搭建项目组件文档

原文链接

juejin.cn/post/749313...

相关推荐
鸿蒙场景化示例代码技术工程师2 分钟前
基于AssetStoreKit实现免密登录鸿蒙示例代码
前端
在掘金3 分钟前
【kk-utils】Excel工具——excel-js
前端·excel
Danny_FD4 分钟前
Canvas的应用与实践
前端·javascript
没事别学JAVA4 分钟前
vue3环境搭建、nodejs22.x安装、yarn 1全局安装、npm切换yarn 1、yarn 1 切换npm
vue.js·node.js·vue
_请输入用户名7 分钟前
husky 切换 simlple-git-hook 失效解决方法
前端
前端九哥7 分钟前
🚀Vue 3 hooks 每次使用都是新建一个实例?一文彻底搞懂!🎉
前端·vue.js
AronTing7 分钟前
09-RocketMQ 深度解析:从原理到实战,构建可靠消息驱动微服务
后端·面试·架构
盏灯7 分钟前
尤雨溪搞响应式为什么要从 Object.defineProperty 换成 Proxy❓
前端·vue.js
爱上大树的小猪7 分钟前
【前端样式】使用CSS Grid打造完美响应式卡片布局:auto-fill与minmax深度指南
前端·css·面试
代码小学僧8 分钟前
🤗 赛博佛祖 Cloudflare 初体验托管自定义域名与无限邮箱注册
前端·serverless·云计算