关于瀑布流布局

346 阅读2分钟

一、瀑布流布局核心原理

1.1 布局特点分析

  • 视觉特征:错落有致的多栏结构,元素按垂直方向紧密排列
  • 核心算法:动态计算元素插入的最短列
  • 性能指标:FCP(首次内容渲染)<300ms,CLS(累积布局偏移)<0.1

1.2 与传统布局对比

特性瀑布流网格布局
元素高度动态不规则固定或比例
渲染顺序垂直优先水平优先
滚动体验连续性加载分页加载
内存占用较高(需计算位置)较低

二、纯CSS实现方案

2.1 CSS Grid进阶用法

<div class="waterfall-grid">
  <div class="item" v-for="item in list" :key="item.id">
    <img :src="item.image" />
    <h3>{{ item.title }}</h3>
  </div>
</div>
.waterfall-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  grid-auto-rows: 50px; /* 基础行高 */
  grid-gap: 20px;
}

.item {
  grid-row-end: span var(--row-span); /* 动态计算跨度 */
}

/* 图片加载后计算行跨度 */
item img {
  width: 100%;
  height: auto;
  display: block;
}

优化技巧

// 监听图片加载完成
const observer = new ResizeObserver(entries => {
  entries.forEach(entry => {
    const rowSpan = Math.ceil(entry.contentRect.height / 50);
    entry.target.style.setProperty('--row-span', rowSpan);
  });
});

document.querySelectorAll('.item').forEach(item => {
  observer.observe(item);
});

2.2 CSS Columns特性

.waterfall-columns {
  column-count: 4; /* 列数 */
  column-gap: 20px;
}

.item {
  break-inside: avoid; /* 防止元素被分割 */
  margin-bottom: 20px;
}

@media (max-width: 1200px) {
  .waterfall-columns {
    column-count: 3;
  }
}

三、JavaScript动态计算方案

3.1 核心算法实现

class Waterfall {
  constructor(container, options) {
    this.container = container;
    this.columnCount = options.columns || 4;
    this.gap = options.gap || 20;
    this.colHeights = new Array(this.columnCount).fill(0);
    this.observeImages();
  }

  // 计算元素位置
  positionElement(el) {
    const minHeight = Math.min(...this.colHeights);
    const columnIndex = this.colHeights.indexOf(minHeight);
    
    const left = columnIndex * (el.offsetWidth + this.gap);
    const top = minHeight + this.gap;
    
    el.style.transform = `translate(${left}px, ${top}px)`;
    this.colHeights[columnIndex] = top + el.offsetHeight;
    
    this.container.style.height = Math.max(...this.colHeights) + 'px';
  }

  // 图片加载监听
  observeImages() {
    const io = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          io.unobserve(img);
        }
      });
    });

    this.container.querySelectorAll('img[data-src]').forEach(img => {
      io.observe(img);
    });
  }
}

3.2 滚动加载优化

class Waterfall {
  // ...

  initScrollLoad() {
    let loading = false;
    
    const checkScroll = () => {
      if (loading) return;
      
      const scrollBottom = window.innerHeight + window.scrollY;
      const triggerPoint = this.container.offsetHeight * 0.8;
      
      if (scrollBottom >= triggerPoint) {
        loading = true;
        this.loadMore().finally(() => loading = false);
      }
    };

    // 防抖处理
    const debounceCheck = debounce(checkScroll, 100);
    window.addEventListener('scroll', debounceCheck);
    window.addEventListener('resize', debounceCheck);
  }

  async loadMore() {
    const newItems = await fetch('/api/items');
    this.appendItems(newItems);
  }
}

// 防抖函数
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

四、企业级优化策略

4.1 虚拟滚动技术

class VirtualWaterfall {
  constructor() {
    this.visibleItems = [];
    this.paddingTop = 0;
    this.paddingBottom = 0;
  }

  calculateVisibleRange() {
    const scrollTop = this.container.scrollTop;
    const viewportHeight = this.container.clientHeight;
    
    const startIdx = Math.floor(scrollTop / this.avgItemHeight);
    const endIdx = Math.ceil((scrollTop + viewportHeight) / this.avgItemHeight);
    
    this.visibleItems = this.fullItems.slice(startIdx, endIdx);
    this.paddingTop = startIdx * this.avgItemHeight;
    this.paddingBottom = (this.fullItems.length - endIdx) * this.avgItemHeight;
  }
}

4.2 性能监控指标

const perfObserver = new PerformanceObserver(list => {
  const entries = list.getEntries();
  entries.forEach(entry => {
    console.log('布局耗时:', entry.duration);
  });
});

perfObserver.observe({ entryTypes: ['layout-shift'] });

// 监控CLS
const clsTracker = new webVitals.getCLS(console.log);

五、框架集成方案(Vue示例)

5.1 自定义指令实现

// waterfall.js
export default {
  mounted(el, binding) {
    const options = binding.value || {};
    const waterfall = new Waterfall(el, {
      columns: options.columns || 4,
      gap: options.gap || 20
    });
    
    el._waterfall = waterfall;
  },
  updated(el) {
    el._waterfall?.updateLayout();
  },
  unmounted(el) {
    el._waterfall?.destroy();
  }
}

5.2 组件化封装

<template>
  <div v-waterfall="{ columns: 4 }" class="waterfall-container">
    <div v-for="item in visibleItems" :key="item.id" class="waterfall-item">
      <img :data-src="item.image" />
      <div class="item-content">{{ item.title }}</div>
    </div>
    <div v-if="loading" class="loading">加载中...</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      loading: false,
      page: 1,
      visibleItems: []
    }
  },
  methods: {
    async loadItems() {
      this.loading = true;
      const newItems = await fetch(`/api/items?page=${this.page}`);
      this.visibleItems = [...this.visibleItems, ...newItems];
      this.page++;
      this.loading = false;
    }
  }
}
</script>