更新 2026年8月16日入门前端工程实践

用 react-photo-album 给生图图库做瀑布流排版

记录在 Magic Img 生图图库中引入 react-photo-album 实现 Masonry 瀑布流的过程:技术选型背景、需求梳理、安装使用,以及「忘记引入样式表导致一张一列」等踩坑与排查方法。

Reactreact-photo-album瀑布流前端Tailwind
用 react-photo-album 给生图图库做瀑布流排版

技术背景

Magic Img 是我做的一个 AI 生图提示词管理工具(自托管、本地优先),其中「生图图库」页面要把所有生成图片以图片墙的形式展示出来。图片墙有两个天然矛盾:

  1. 等宽网格(CSS Grid):整齐,但每行高度由最高的那张决定——不同比例的图片混排时,矮图下面会留下大洞,看起来"很乱";
  2. 原生比例展示:图库必须不裁剪(这是产品约束),所以不能用 object-cover 强行统一比例。

业内的标准解法是 Masonry 瀑布流:按列紧密堆放,每张图保持原始比例,列间高度错落但整体紧凑。实现瀑布流的常见方案有三类:

方案依赖顺序说明
CSS columns 多列纵向阅读序(先下后右)最新图片不再出现在左上角,时间语义丢失
grid row-span 自研保持需固定列数 + 固定行高,逻辑要自己写
react-photo-album~4KB,无依赖基本保持(逐列最短列填充)专为相册墙设计,TS 友好,维护活跃

最终选了 react-photo-album v3:体积小、类型齐全,而且它的 Masonry 模式恰好需要「每张图的 width/height」——我们的数据库里本来就存了,零额外成本。

需求梳理

  1. 展示全部生成图(21 张起,会持续增长),原比例、不裁剪
  2. 不同尺寸(2304×1728、4608×3456 等)混排时紧凑、不留洞
  3. 顺序基本保持「新图靠前」;
  4. 点击任意一张打开大图预览(YARL 灯箱),悬停显示所属提示词;
  5. 平板性能:墙上只用 300px 缩略图,原图只进灯箱按需加载。

安装与使用

1. 安装

npm install react-photo-album

2. 准备数据(关键输入是宽高)

库要求的 Photo 对象只有三个必需字段:srcwidthheight。我们的接口本来就返回每张图的原始宽高:

import PhotoAlbum, { type Photo } from "react-photo-album"
// 必须引入样式表!容器/列的布局规则都在这里(坑 1 详述)
import "react-photo-album/styles.css"

interface GalleryPhoto extends Photo {
  prompt_text?: string
}

const albumPhotos: GalleryPhoto[] = images.map((img) => ({
  key: String(img.id),
  src: assetUrl(img.thumb_path) || assetUrl(img.file_path),
  width: img.width,
  height: img.height,
  alt: img.prompt_text || `生成图 ${img.id}`,
  prompt_text: img.prompt_text,
}))

3. 使用 Masonry 布局

<PhotoAlbum
  layout="masonry"
  columns={(cw) => (cw < 640 ? 2 : cw < 960 ? 3 : cw < 1280 ? 4 : 5)}
  spacing={16}
  padding={0}
  photos={albumPhotos}
  onClick={({ index }) => openPreview(index)}
  render={{
    photo: (props, context) => {
      const { onClick } = props
      const { photo, index, width, height } = context
      return (
        <button type="button" onClick={onClick} className="group relative block w-full ...">
          <img
            src={photo.src}
            alt={photo.alt || ""}
            decoding="async"
            style={{ width, height }}
          />
          {/* hover 提示词浮层 */}
          <div className="... opacity-0 group-hover:opacity-100">
            {photo.prompt_text}
          </div>
        </button>
      )
    },
  }}
/>

几个要点:

  • columns 可以是数字或 (containerWidth) => number 函数,做响应式列数很方便;
  • 点击回调 onClick 里拿到的是原始数组下标 index,直接传给灯箱即可;
  • 自定义 render.photo 后,图片尺寸要自己用 context 里的 width/height(渲染尺寸)设置——这两个值库已经按列宽算好了。

遇到的坑

坑 1:忘记引入样式表 → 「一张一列」

这是最坑的一个。装完包、写好 <PhotoAlbum layout="masonry">,页面渲染出来却是:每张图占一整行,右侧大片置灰空白

排查过程:先怀疑是 columns 函数没生效,改成固定数字无效;再检查 DOM——容器和 5 个列(track)都在,列的宽度也算对了,但图片全都堆在布局里。

最后翻包的 package.json 发现它导出了独立的 CSS 文件(./styles.css./masonry.css 等),而官方 README 示例都带了一行 import "react-photo-album/styles.css"。容器是 flex、列是 flex-column、列宽按 --react-photo-album--columns 计算——这些规则全在这个样式表里,不引入就退化成普通块级堆叠。

解决:顶部加一行:

import "react-photo-album/styles.css"

经验:用这类"无运行时依赖"的布局库,先看它的 exports 里有没有独立 CSS,有就一定要引。

坑 2:render.photo(props, context) 双参签名

自定义渲染时我凭直觉写了:

photo: ({ photo, index, width, height, onClick }) => ... // ❌ 报错:Property 'photo' does not exist

TS 报错提示 photo 等属性不存在于 RenderPhotoProps。看类型定义才发现 v3 的渲染函数是两个参数

type RenderFunction<Props, Context> = (props: Props, context: Context) => ReactNode
// photo 的渲染上下文(photo/index/width/height)在第二个参数里
photo: (props, context) => {
  const { onClick } = props
  const { photo, index, width, height } = context
  ...
}

解决:按 (props, context) 拆解即可。装新库先扫一眼 .d.ts 里的 Render 类型签名,能少走很多弯路。

坑 3:测试断言把「DOM 顺序」当成了「视觉顺序」

布局修好后,我在浏览器自测脚本里加了一条断言:取前两个 <img>,比较它们的左坐标,认为应该并排(不同列)。

结果断言失败——但页面明明是 5 列并排。排查发现:瀑布流的 DOM 是按"列"组织的(第 1 列的全部图片在前,第 2 列的在后),所以 img[0]img[1] 属于同一列,左坐标当然相同。

解决:断言改为比较「第 1 列的首张图 vs 第 2 列的首张图」:

const a = tracks[0].querySelector("img")
const b = tracks[1].querySelector("img")
return Math.abs(a.getBoundingClientRect().left - b.getBoundingClientRect().left) > 10

经验:验证布局类库时,先搞清楚它生成的 DOM 结构,再写断言;另外把「真实浏览器探针」当排查手段(直接打印各 track 的 x 坐标和子元素数量),比盯着代码猜快得多。

坑 4:等宽网格的"洞"(引入前就存在的产品问题)

顺带记录一下为什么换掉原来的布局:CSS Grid repeat(auto-fill, minmax(220px, 1fr)) 等宽铺满,图片 w-full h-auto。当一行里混有不同比例时,行高取最高的那张,矮图底部留空,几行错落下来就显得乱;另外窗口宽度"差一点放不下下一张"时,右侧会空出近一整列。

换成 Masonry 后这两个问题都消失了——列内紧密堆叠、列数自适应。

效果与小结

最终效果:21 张不同比例的生成图在 5 列瀑布流里紧密排列,原比例不裁剪,悬停出提示词浮层,点击进 YARL 大图预览;缩略图总大小仅 360KB,进页面即全量加载 + immutable 长缓存,平板上快速滚动也跟手。

一句话总结:react-photo-album 是个小但专业的相册布局库,只要记住三件事——引样式表、喂宽高、render 函数是双参——就能在十分钟内把图片墙升级成真正的瀑布流。

评论