# 一、瀑布流布局(列数目固定)

  瀑布流的实现方式有很多种,可以根据不同的应用场景选择不同的方案:

# 图片高度获取

<script>
  new Vue({
    el: '#app',
    data: {
      imgDataList: []
    },
    async created() {
      const { data } = await axios.get('https://picsum.photos/v2/list')
      // this.dataList = data
      const dataHandle = []

      data.forEach((item) => {
        let img = new Image()
        img.src = item.download_url

        img.onload = function () {
          item.picWidth = this.width
          item.picHeight = this.height
          dataHandle.push(item)
        }
      })

      this.imgDataList = dataHandle
    }
  })
</script>

# 1、多列布局

  使用多列布局的特点是,有 CSS 确定固定的列数(column-count)、宽度(column-width)等

特点:先分为固定列数,然后按照一列一列的获取数据

提示

  由于它的渲染特性,在正式的开发中不推荐使用(数据非常少,分页式的场景可以尝试使用 😂)。

在线演示 (opens new window)

.waterfall-container {
  column-count: 2; /* 列数 */
  column-gap: 1vw; /* 列间距 */

  /*   防止被错位截断 */
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
  page-break-inside: avoid;
  column-fill: balance-all;
}

.waterfall-item {
  padding: 2vw;
  /*   margin: 2vw; */
  margin-bottom: 2vw;

  /*   防止被错位截断 */
  height: 100%;
  overflow: auto;
}

# 2、grid 布局 + js ✨

  grid 布局和多列布局实现瀑布流布局的方式几乎一致,不同的是,grid 布局渲染的数据是:从左到右依次渲染的。

提示

  尽管当前方式可以解决基本的瀑布流布局(特别是移动端)问题,但还是存在不少问题:

  • 依旧无法动态的决定使用几列
  • 后端得返回图片的 width、height(前端性能消耗会比较大)
  • itemAsyncHeight 的计算结果会受到 padding、margin 的影响

在线演示 (opens new window)

.waterfall-container {
  display: grid;
  grid-template-columns: repeat(2, 1fr); /* 列数和大小 */
  grid-gap: 1vw; /* 间隔 */
}

其实我们使直接使用一个 grid-template-rows: masonry; 代替 js 操作,但是目前还是实验性功能。

<div id="app">
  <h1>{{nickName}}</h1>
  <div class="waterfall-container">
    <div class="left-column">
      <!-- …… -->
    </div>
    <div class="right-column">
      <!-- …… -->
    </div>
  </div>
</div>
// 也可以封装成一个class类
function getPicHeight(picWidth, picHeight) {
  const screenWidth = window.innerWidth // 屏幕宽度
  const itemPadding = 15
  const itemMargin = 5
  const gridGap = 5
  const gridColumns = 2

  // 1、获取 item 的视图宽度
  const itemAsyncWidth = ~~(
    (screenWidth - (itemPadding + itemMargin) * gridColumns * 2 - gridGap * (gridColumns - 1)) /
    gridColumns
  ) // 取整

  // 2、获取 item 的视图高度
  const itemAsyncHeight = ((picHeight / picWidth) * itemAsyncWidth).toFixed(0) // 取整

  return itemAsyncHeight
}

# 3、flex 布局 + js

  和上面的 grid 布局 + js 实现一样,但它比前者要弱一点的是:对 columns 列数、gap 间隙的管理更加不便。

在线演示 (opens new window)

# 4、绝对定位 + js

  使用绝对定位 + js 方案,实现了对 columns 列数的统一管理,可以适用于 pc 端使用。其缺点是,由于 columns 固定,响应式实现性能消耗较大。

在线演示 (opens new window)

.waterfall-container {
  position: relative;
  width: 100%;
  height: 100%;
}

.waterfall-item {
  position: absolute;
  height: 100%;
}
核心实现
new Vue({
  el: '#app',
  data: {
    title: '瀑布流-绝对定位',
    dataList: [],
    waterfallColumns: 3,
    waterfallGap: 0
  },
  methods: {
    getPicHeight(picWidth, picHeight) {
      const screenWidth = window.innerWidth // 屏幕宽度

      // 1、获取 item 的视图宽度
      const itemAsyncWidth = ~~(
        (screenWidth - this.waterfallGap * (this.waterfallColumns - 1)) /
        this.waterfallColumns
      ) // 取整

      // 2、获取 item 的视图高度
      const itemAsyncHeight = (picHeight / picWidth) * itemAsyncWidth // 取整

      return itemAsyncHeight
    },
    picPositionHandle(data) {
      if (data.length > 0) {
        let columnsHeightArr = new Array(this.waterfallColumns).fill(0)
        this.dataList = []

        data.forEach((item, index) => {
          let arrIndex = columnsHeightArr.indexOf(Math.min.apply(null, columnsHeightArr))

          // item 配置
          item.styleWidth = 100 / this.waterfallColumns + 'vw'
          item.styleLeft = arrIndex * (100 / this.waterfallColumns) + 'vw'
          item.styleTop = columnsHeightArr[arrIndex] + 'px'

          // 列高叠加
          let columnsHeight = this.getPicHeight(item.width, item.height)
          columnsHeightArr[arrIndex] += columnsHeight

          this.dataList.push(item)
        })
      }
    },
    handleResize() {
      // 更新屏幕宽度和高度数据
      this.picPositionHandle(this.dataList)
    }
  },
  async created() {
    const { data } = await axios.get('https://picsum.photos/v2/list')
    // this.dataList = data;
    this.picPositionHandle(data)
  },
  mounted() {
    // 添加窗口大小改变事件监听器
    window.addEventListener('resize', this.handleResize)
  }
})

# 4、绝对定位 + js + flex + 等宽 ✨

  这个是在上个方案的基础上进行改造的。

在线演示 (opens new window)

.waterfall-container {
  display: flex;
  flex-wrap: wrap;

  position: relative;
  width: 100%;
  height: 100%;
}

.waterfall-item {
  position: absolute;
  height: 100%;
}
核心实现
new Vue({
  el: '#app',
  data: {
    title: '瀑布流-绝对定位-flex-等宽',
    dataList: [],
    styleWidth: 200,
    waterfallGap: 0
  },
  methods: {
    picPositionHandle(data) {
      if (data.length > 0) {
        const screenWidth = window.innerWidth // 屏幕宽度
        const waterfallColumns = Math.floor(screenWidth / 200)

        let columnsHeightArr = new Array(waterfallColumns).fill(0)
        this.dataList = []

        data.forEach((item, index) => {
          let arrIndex = columnsHeightArr.indexOf(Math.min.apply(null, columnsHeightArr))

          // item 配置
          item.styleLeft = arrIndex * (100 / waterfallColumns) + 'vw'
          item.styleTop = columnsHeightArr[arrIndex] + 'px'

          // 列高叠加
          let columnsHeight = Number(((item.height / item.width) * 200).toFixed(0))
          columnsHeightArr[arrIndex] += columnsHeight

          this.dataList.push(item)
        })
      }
    },
    handleResize() {
      // 更新屏幕宽度和高度数据
      this.picPositionHandle(this.dataList)
    }
  },
  async created() {
    const { data } = await axios.get('https://picsum.photos/v2/list')
    // this.dataList = data;
    this.picPositionHandle(data)
  },
  mounted() {
    // 添加窗口大小改变事件监听器
    window.addEventListener('resize', this.handleResize)
  }
})

# 5、开源库

https://github.com/jiaozitang/react-masonry-component2#readme

masonry (opens new window)

vue-waterfall (opens new window)

# 二、首屏渲染优化

# 1、懒加载 🎈

  实现懒加载通常有两种方式,一种是自己计算可视窗口与图片的距离并监听,另外一种是使用 IntersectionObserver (opens new window)

<div class="waterfall-container">
  <template v-for="item in dataList" :key="item.id">
    <div class="waterfall-item ">
      <!-- 这里 -->
      <img src="../public/default.png" :data-src="item.download_url" alt="" />
    </div>
  </template>
</div>

在线演示 (opens new window)

简单实现
new Vue({
  async created() {
    // ……

    // =============

    // 图片懒加载
    this.$nextTick(() => {
      const interSectObs = new IntersectionObserver(
        (entries) => {
          for (let entry of entries) {
            // 1、交叉了
            if (entry.isIntersecting) {
              const imgElemt = entry.target
              imgElemt.src = imgElemt.dataset.src // 加载图片

              interSectObs.unobserve(imgElemt)
            }
          }
        },
        { threshold: 0 }
      )
      const imgs = document.querySelectorAll('img[data-src]')
      imgs.forEach((img) => {
        console.log(img)
        interSectObs.observe(img)
      })
    })
  }
})

在线演示 (opens new window)

自定义指令封装版
<div class="waterfall-container">
  <template v-for="item in dataList" :key="item.id">
    <div class="waterfall-item ">
      <!-- 这里 -->
      <img
        v-imglazy="{ islazy }"
        src="../public/default.png"
        :data-src="item.download_url"
        alt=""
      />
    </div>
  </template>
</div>
Vue.directive('imglazy', function (el, binding) {
  const { islazy } = binding.value

  if (!islazy) return (el.src = el.dataset.src)

  const interSectObs = new IntersectionObserver((entries) => {
    if (entries[0].intersectionRatio <= 0) return

    el.src = el.dataset.src
    el.unobserve(imgElemt)
  })
  interSectObs.observe(el)
})

# 2、加动画

  以使用 Animate.css 为例:

<div class="waterfall-container">
  <template v-for="item in dataList" :key="item.id">
    <!-- 这里 -->
    <div class="waterfall-item animate__animated animate__bounceIn animate__delay-2s">
      <img :src="item.download_url" alt="" />
    </div>
  </template>
</div>

# 3、并行渲染

简单一句话:同时加载多个图片资源并在加载完成后显示它们

更新于 : 7/8/2024, 10:21:14 AM