在 React 中封装瀑布流布局的完整指南

155 阅读2分钟

在 React 中封装瀑布流布局的完整指南

瀑布流布局(Masonry Layout)是一种常见的网页布局方式,适用于展示不等高的图片或卡片内容。在 React 中实现瀑布流布局可以通过多种方式完成,本文将详细介绍如何封装一个灵活且高性能的瀑布流组件。


一、核心概念

1.1 瀑布流布局特点

  • 错落排列:元素按列堆叠,每列高度不同,形成瀑布式效果。
  • 动态计算:根据容器宽度动态调整列数和元素位置。
  • 响应式支持:适应不同屏幕尺寸,自动调整布局。

1.2 实现方式

  • CSS 原生方案:使用 CSS GridFlexbox
  • 第三方库:如 react-masonry-cssreact-masonry-infinite
  • 自定义实现:结合 JavaScript 动态计算位置。

二、使用 CSS Grid 封装瀑布流

2.1 初始化项目

npx create-react-app masonry-demo
cd masonry-demo
npm install tailwindcss --save-dev
npx tailwindcss init -p

2.2 配置 Tailwind CSS

tailwind.config.js 中添加:

module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

src/index.css 中引入 Tailwind:

@tailwind base;
@tailwind components;
@tailwind utilities;

2.3 创建瀑布流组件

// src/components/MasonryGrid.jsx
import React, { useCallback, useEffect, useState } from "react";

const MasonryGrid = ({ items }) => {
  const [cols, setCols] = useState(3);

  const resetCols = () => {
    const width = window.innerWidth;
    setCols(Math.max(1, Math.floor(width / 300))); // 每列最小宽度 300px
  };

  useEffect(() => {
    resetCols();
  }, []);

  const handleResize = useCallback(() => {
    resetCols();
  }, []);

  useEffect(() => {
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, [handleResize]);

  return (
    <div className="grid grid-cols-auto-fit-300 gap-4">
      {items.map((item, index) => (
        <div key={index} className="relative w-full">
          <img
            src={item}
            alt={`item-${index}`}
            className="w-full rounded-lg shadow-md"
          />
        </div>
      ))}
    </div>
  );
};

export default MasonryGrid;

2.4 使用组件

// src/App.jsx
import React from "react";
import MasonryGrid from "./components/MasonryGrid";

const App = () => {
  const images = [
    "https://source.unsplash.com/random/400x300?sig=1",
    "https://source.unsplash.com/random/400x400?sig=2",
    "https://source.unsplash.com/random/400x500?sig=3",
    // 添加更多图片...
  ];

  return (
    <div className="p-4">
      <h1 className="text-2xl font-bold mb-4">Masonry Layout</h1>
      <MasonryGrid items={images} />
    </div>
  );
};

export default App;

三、动态高度与虚拟滚动优化

3.1 处理图片动态高度

如果图片高度不确定,需要在图片加载后计算实际高度:

const MasonryGrid = ({ items }) => {
  const [columnHeights, setColumnHeights] = useState([0, 0, 0]);
  const [positions, setPositions] = useState([]);

  useEffect(() => {
    const newPositions = [];
    const newHeights = [...columnHeights];

    items.forEach((item, index) => {
      const shortestCol = newHeights.indexOf(Math.min(...newHeights));
      newPositions.push({ col: shortestCol, top: newHeights[shortestCol] });
      newHeights[shortestCol] += 300; // 假设图片高度为 300px
    });

    setPositions(newPositions);
    setColumnHeights(newHeights);
  }, [items, columnHeights]);

  return (
    <div className="flex">
      {[...Array(3)].map((_, colIndex) => (
        <div key={colIndex} className="w-1/3 p-2">
          {items
            .filter((_, index) => positions[index]?.col === colIndex)
            .map((item, index) => (
              <div
                key={index}
                style={{ marginTop: positions[index]?.top }}
                className="mb-4"
              >
                <img
                  src={item}
                  alt={`item-${index}`}
                  className="w-full rounded-lg shadow-md"
                />
              </div>
            ))}
        </div>
      ))}
    </div>
  );
};

3.2 虚拟滚动优化

对于大量数据,使用虚拟滚动减少 DOM 渲染:

import { useState, useEffect, useRef } from "react";

const VirtualMasonry = ({ items }) => {
  const containerRef = useRef(null);
  const [visibleItems, setVisibleItems] = useState([]);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            setVisibleItems((prev) => [...prev, entry.target.dataset.index]);
          }
        });
      },
      { rootMargin: "0px", threshold: 0.1 }
    );

    items.forEach((_, index) => {
      const element = document.getElementById(`item-${index}`);
      if (element) observer.observe(element);
    });

    return () => observer.disconnect();
  }, [items]);

  return (
    <div ref={containerRef} className="flex">
      {[...Array(3)].map((_, colIndex) => (
        <div key={colIndex} className="w-1/3 p-2">
          {items
            .filter((_, index) => visibleItems.includes(index.toString()))
            .map((item, index) => (
              <div key={index} id={`item-${index}`} className="mb-4">
                <img
                  src={item}
                  alt={`item-${index}`}
                  className="w-full rounded-lg shadow-md"
                />
              </div>
            ))}
        </div>
      ))}
    </div>
  );
};

四、响应式设计

4.1 媒体查询适配

通过 window.matchMedia 动态调整列数:

const useResponsiveColumns = () => {
  const [cols, setCols] = useState(3);

  useEffect(() => {
    const mediaQueries = [
      window.matchMedia("(max-width: 600px)"),
      window.matchMedia("(max-width: 900px)"),
    ];

    const updateColumns = () => {
      if (mediaQueries[0].matches) setCols(1);
      else if (mediaQueries[1].matches) setCols(2);
      else setCols(3);
    };

    mediaQueries.forEach((mq) => mq.addEventListener("change", updateColumns));
    updateColumns();

    return () => {
      mediaQueries.forEach((mq) =>
        mq.removeEventListener("change", updateColumns)
      );
    };
  }, []);

  return cols;
};

在组件中使用:

const MasonryGrid = ({ items }) => {
  const cols = useResponsiveColumns();
  // ...
};

五、性能优化

5.1 图片懒加载

使用 IntersectionObserver 实现懒加载:

const LazyImage = ({ src, alt }) => {
  const imgRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            const img = entry.target;
            img.src = src;
            observer.unobserve(img);
          }
        });
      },
      { rootMargin: "0px", threshold: 0.1 }
    );

    if (imgRef.current) observer.observe(imgRef.current);

    return () => {
      if (imgRef.current) observer.unobserve(imgRef.current);
    };
  }, [src]);

  return <img ref={imgRef} alt={alt} className="w-full rounded-lg shadow-md" />;
};

5.2 防抖处理

在窗口大小变化时添加防抖:

const useDebouncedResize = (callback, delay) => {
  const debounced = useRef(null);

  useEffect(() => {
    const handler = () => {
      clearTimeout(debounced.current);
      debounced.current = setTimeout(callback, delay);
    };

    window.addEventListener("resize", handler);
    return () => window.removeEventListener("resize", handler);
  }, [callback, delay]);
};

六、完整示例

6.1 最终组件代码

// src/components/MasonryGrid.jsx
import React, { useState, useEffect, useRef } from "react";

const MasonryGrid = ({ items }) => {
  const [cols, setCols] = useState(3);
  const [columnHeights, setColumnHeights] = useState([0, 0, 0]);
  const [positions, setPositions] = useState([]);

  const resetCols = () => {
    const width = window.innerWidth;
    setCols(Math.max(1, Math.floor(width / 300)));
  };

  useEffect(() => {
    resetCols();
  }, []);

  const handleResize = () => {
    resetCols();
  };

  useEffect(() => {
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  useEffect(() => {
    const newPositions = [];
    const newHeights = [...columnHeights];

    items.forEach((item, index) => {
      const shortestCol = newHeights.indexOf(Math.min(...newHeights));
      newPositions.push({ col: shortestCol, top: newHeights[shortestCol] });
      newHeights[shortestCol] += 300; // 假设图片高度为 300px
    });

    setPositions(newPositions);
    setColumnHeights(newHeights);
  }, [items, columnHeights]);

  return (
    <div className="flex">
      {[...Array(cols)].map((_, colIndex) => (
        <div key={colIndex} className="w-1/3 p-2">
          {items
            .filter((_, index) => positions[index]?.col === colIndex)
            .map((item, index) => (
              <div
                key={index}
                style={{ marginTop: positions[index]?.top }}
                className="mb-4"
              >
                <img
                  src={item}
                  alt={`item-${index}`}
                  className="w-full rounded-lg shadow-md"
                />
              </div>
            ))}
        </div>
      ))}
    </div>
  );
};

export default MasonryGrid;

6.2 使用组件

// src/App.jsx
import React from "react";
import MasonryGrid from "./components/MasonryGrid";

const App = () => {
  const images = Array.from({ length: 20 }, (_, i) =>
    `https://source.unsplash.com/random/400x${300 + i * 10}?sig=${i}`
  );

  return (
    <div className="p-4">
      <h1 className="text-2xl font-bold mb-4">Masonry Layout</h1>
      <MasonryGrid items={images} />
    </div>
  );
};

export default App;

七、总结

在 React 中封装瀑布流布局的核心在于:

  1. 动态计算列数和位置:根据容器宽度和元素高度调整布局。
  2. 响应式设计:通过媒体查询或窗口监听实现自适应布局。
  3. 性能优化:使用虚拟滚动、懒加载和防抖技术减少资源消耗。

通过上述方法,你可以创建一个高效且灵活的瀑布流组件,适用于图片展示、社交媒体动态等场景。根据实际需求选择合适的实现方式(CSS 原生、第三方库或自定义逻辑),并结合性能优化技巧,确保用户体验流畅。