Ant Design Masonry 瀑布流组件完全指南:从响应式配置到源码级布局算法
【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/GitHub_Trending/an/ant-design
本文是 Ant Design(当前仓库)中
Masonry瀑布流组件的技术指南。Masonry 是一个"等宽但不等高、按列数均匀排布"的瀑布流布局组件,用于高效展示图片墙、卡片流等不规则高度内容。读完本文,你将掌握 Masonry 的全部 API(columns、gutter、items、itemRender、fresh、onLayoutChange等)、响应式列数与间距配置方法、图片/动态内容场景下的尺寸监听机制,以及其背后"最矮列优先"的经典瀑布流布局算法在源码中的实现细节。
一、组件定位与使用时机
Masonry 属于布局类组件,其组件文档(components/masonry/index.zh-CN.md)给出的定位是:瀑布流布局组件,用于展示不同高度的内容。与传统的多行栅格不同,瀑布流要求每一列都能"紧凑地"叠放高低不一的条目,行尾不再对齐,从而最大化利用纵向空间。
从文档看,它最适合以下三类场景:
- 展示不规则高度的图片或卡片时(典型如照片墙、瀑布流信息流);
- 需要按照列数均匀分布内容时;
- 需要响应式调整列数时(窄屏一列、宽屏多列)。
值得一提的前提:组件文档的元信息tag: 6.0.0表明该组件及其语义化样式能力自该版本起提供,使用前请确认你所依赖的 antd 版本包含 Masonry(可在当前仓库 components/index.ts 中确认其导出关系)。
二、快速上手:一个最简瀑布流
Masonry 的核心用法是把"高度数据"通过items传入,并用itemRender决定每个条目如何渲染。以下节选自官方示例 components/masonry/demo/basic.tsx:
import React from 'react'; import { Card, Masonry } from 'antd'; import type { MasonryProps } from 'antd'; type MasonryItemType = NonNullable<MasonryProps<number>['items']>[number]; // 每个条目的高度数据(实际场景中通常来自接口) const heights = [150, 50, 90, 70, 110, 150, 130, 80, 50, 90, 100, 150, 60, 50, 80]; const App: React.FC = () => ( <Masonry columns={4} gutter={16} items={heights.map((height, index) => ({ key: `item-${index}`, data: height }))} itemRender={({ data, index }) => ( <Card size="small" style={{ height: data }}> {index + 1} </Card> )} /> ); export default App;把这份代码跑起来,你就能看到 15 张高度各异的小卡片被自动分配到 4 列中:新增的卡片总会被塞进"当前最短的那一列"。示例还演示了一种常见诉求——某个条目需要完全自定义内容:此时直接给对应 item 设置children,它的渲染优先级高于itemRender(源码见 MasonryItem.tsx 中item.children ?? itemRender?.(...)的判断逻辑)。这样你就既可以用统一itemRender批量生成条目,又可以为特殊 item 单独定制 DOM。
三、完整 API 参考
Masonry 复用 antd 的通用属性(className、style、prefixCls、rootClassName等),并定义了自己专属的 props 与子组件。下面的参数表与说明以官方中文文档为准,并结合源码对每个参数的实际效果做了补充。
Masonry 主组件
| 参数 | 说明 | 类型 | 默认值 | 版本 | 全局配置 |
|---|---|---|---|---|---|
| classNames | 自定义组件内部各语义化结构的 class,支持对象或函数 | Record<SemanticDOM, string> \| ((info: { props }) => Record<SemanticDOM, string>) | - | 6.0.0 | 6.0.0 |
| columns | 列数,可以是固定值或响应式配置 | number \| { xs?; sm?; md? } | 3 | - | × |
| fresh | 是否持续监听子项尺寸变化 | boolean | false | - | × |
| gutter | 间距,固定值、响应式配置或水平/垂直二元组 | Gap \| [Gap, Gap] | 0 | - | × |
| items | 瀑布流项 | MasonryItem[] | - | - | × |
| itemRender | 自定义项渲染 | (item: MasonryItem) => React.ReactNode | - | - | × |
| styles | 语义化结构 style,支持对象和函数 | Record<SemanticDOM, CSSProperties> \| ((info: { props }) => Record<SemanticDOM, CSSProperties>) | - | 6.0.0 | 6.0.0 |
| onLayoutChange | 列排序回调 | ({ key: React.Key; column: number }[]) => void | - | - | × |
表中"全局配置"列打
✓(即 classNames / styles)的项,可通过 ConfigProvider 的componentConfig针对全局统一注入,Masonry 内部通过useComponentConfig('masonry')读取(见 Masonry.tsx);打×的项(如列数、间距、数据)仅支持组件级配置。
MasonryItem(items 的元素)
items中的每一项在类型上就是MasonryItem(MasonryItem.tsx),完整字段如下:
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| children | 自定义展示内容,相对itemRender具有更高优先级 | React.ReactNode | - |
| column | 自定义所在列 | number | - |
| data | 自定义存储数据(如高度、图片地址),透传给itemRender | T | - |
| height | 高度 | number | - |
| key | 唯一标识 | string \| number | - |
需要特别说明的是data:它不是渲染内容,而是"数据载荷",最终会出现在itemRender回调的参数里。例如图片场景中常把图片 URL 存进data,渲染时再取出,见下文的图片示例。
Gap 间距类型
官方文档给出gutter的取值类型为:
type Gap = undefined | number | Partial<Record<'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl', number>>;也就是说:
gutter={16}:所有间隙固定 16px;gutter={{ xs: 8, sm: 12, md: 16 }}:随屏幕断点变化;gutter={[16, 24]}:水平间隙 16px、垂直间隙 24px(水平/垂直不对称的二元组写法)。
Gap 与 columns 的区别值得留意:columns 目前只按列维度把内容均匀铺开,而 gutter 复用的是 GridRow的 gutter 语义——从源码看gutter?: RowProps['gutter'](Masonry.tsx),并通过 grid 的useGutter在对应断点解析出[horizontalGutter, verticalGutter],其中垂直间距同时参与列高的累加计算(见下文原理章节)。
四、响应式:按断点切换列数与间距
真实业务里,瀑布流必须适配手机、平板和桌面。Masonry 把列数、间距都做成了响应式配置,见官方示例 components/masonry/demo/responsive.tsx:
import React from 'react'; import { Card, Masonry } from 'antd'; const heights = [120, 55, 85, 160, 95, 140, 75, 110, 65, 130, 90, 145, 55, 100, 80]; const App: React.FC = () => { const items = heights.map((height, index) => ({ key: `item-${index}`, data: height, })); return ( <Masonry columns={{ xs: 1, sm: 2, md: 3, lg: 4 }} gutter={{ xs: 8, sm: 12, md: 16 }} items={items} itemRender={(item) => ( <Card size="small" style={{ height: item.data }}> {item.index + 1} </Card> )} /> ); };官方 API 表格把columns的响应式类型写为{ xs?; sm?; md? },但请注意响应式示例本身就用到了lg: 4。结合源码确认:columns?: number | Partial<Record<Breakpoint, number>>(Masonry.tsx),而Breakpoint枚举自responsiveObserver的responsiveArray(components/_util/responsiveObserver.ts),即xxxl / xxl / xl / lg / md / sm / xs全量断点都是可用的。
列数的解析逻辑也很讲究(Masonry.tsx):
- 未传
columns时默认返回3; - 传数字时直接返回该数字;
- 传对象时,按
responsiveArray的顺序(从大到小)找到第一个当前屏幕命中且已配置列数的断点;一个都没命中则回退columns.xs ?? 1。
因此{ xs: 1, sm: 2, md: 3, lg: 4 }在宽屏取 4、平板取 3、窄屏依次降为 2、1,整个切换由useBreakpoint驱动,无需手动写 media query。
五、图片墙场景:自动感知图片加载完成后的尺寸
瀑布流最常见的内容就是图片——而图片恰恰有一个天然难点:加载完成前浏览器不知道它的真实高度。Masonry 对此做了两层处理,源码集中在 Masonry.tsx:
- 容器最外层包裹
ResizeObserver,容器尺寸变化(例如窗口缩放)会触发重排; - 容器上直接绑定了
onLoad与onError,借助 React 的事件冒泡,任何一张图片加载完成或加载失败都会触发一次"重新测量尺寸"。
于是图片墙可以写得非常干净。官方图片示例 components/masonry/demo/image.tsx 的渲染逻辑只有两件事——把 URL 放进data,渲染时铺满宽度并等待高度被测量:
import React from 'react'; import { Masonry } from 'antd'; const imageList = [ 'https://images.unsplash.com/photo-1510001618818-4b4e3d86bf0f', // ... 若干图片 URL ]; const App = () => ( <Masonry columns={4} gutter={16} items={imageList.map((img, index) => ({ key: `item-${index}`, data: img, }))} itemRender={({ data }) => ( <img src={`${data}?w=523&auto=format`} alt="sample" style={{ width: '100%' }} /> )} /> );测量是异步合并的:collectItemSize通过useDelay包装(components/masonry/hooks/useDelay.ts),内部基于@rc-component/util的raf做 requestAnimationFrame 节流——同一帧内多次触发的尺寸变化只会合并成一次重排,避免图片逐张加载时频繁计算导致的抖动与性能浪费。测量时组件会遍历所有已挂载的 item,读取其getBoundingClientRect().height(每个 item 的真实 DOM 引用由 useRefs.ts 维护的Map<key, element>提供),只在高度集合真正变化时才触发位置重算。
六、动态增删条目:配合 onLayoutChange 保持位置
瀑布流信息流往往需要"上拉加载更多""手动删除卡片"之类的动态操作。Masonry 的默认策略是把条目稳定地按序放进各列,这本身就能避免频繁增删引起的整片跳动;当需要更精细地控制"每个条目落在第几列"时,可以借助onLayoutChange把计算结果"写回"items。
官方动态示例 components/masonry/demo/dynamic.tsx 的做法是:
- 初始化时为每条数据显式指定
column: index % 4,让内容均匀分到 4 列; - 通过
onLayoutChange回调拿到最新的{ key, column }映射(回调签名即({ key: React.Key; column: number }[]) => void),并把新的列号写回 state; - 添加新卡片时不再指定
column(由算法自动寻找最矮列),删除卡片时按key过滤即可。
<Masonry columns={4} gutter={16} items={items} onLayoutChange={(sortedItems) => { setItems((prevItems) => prevItems.map((item) => { const matchItem = sortedItems.find((sortedItem) => sortedItem.key === item.key); return matchItem ? { ...item, column: matchItem.column } : item; }), ); }} ... />从源码看,onLayoutChange的触发有两条链路(Masonry.tsx):先由useLayoutEffect在"所有条目都拿到 position"时把最新的[item, column]列表缓存下来,再在 items 数量与缓存一致时向外抛出。这一机制保证了回调中的顺序、数量与当前渲染保持一致,开发者可以放心用它做受控列号的同步。其位置计算的另一条铁律见 usePositions.ts 的注释:"Always get stable positions by order instead of dynamic adjust for next item height"——即永远按 items 的原始顺序为条目分配位置,绝不会因为后一个条目的高度去调整前一个条目的位置,这从根上保证了增删 item 时的视觉稳定。
七、fresh 模式:持续监听子项内部尺寸变化
需要澄清一个易混点:常规模式下,组件的尺寸监听并不是"每时每刻"都开着的。默认fresh={false}时,组件只在 items 变化、列数变化、容器 ResizeObserver 触发以及图片 load/error 等关键时机重新测量。这对绝大多数静态内容(卡片、普通图片)已经足够且性能更优。
但如果卡片内部包含会在渲染之后才改变尺寸的内容(例如懒加载的富文本、折叠展开区、动态插入的媒体),就需要开启:
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| fresh | 是否持续监听子项尺寸变化 | boolean | false |
fresh的底层开关非常直观(Masonry.tsx):开启后每个 item 会被额外的ResizeObserver包裹,其onResize持续驱动collectItemSize;关闭时该 observer 不挂载(对应 MasonryItem.tsx 中"仅在onResize存在时才包裹 ResizeObserver"的惰性逻辑)。由于 demo 列表中以 debug 方式标注了"持续更新"示例(components/masonry/demo/fresh.tsx),建议先在本地验证你的内容场景是否真的需要 fresh,再决定是否开启,避免不必要的持续监听开销。
八、语义化结构与样式定制:classNames / styles / Semantic DOM
Masonry 的样式定制遵循 antd 的"语义化 DOM + classNames/styles"体系。组件结构为两层(定义见 Masonry.tsx,交互式示意见 components/masonry/demo/_semantic.tsx):
| 语义节点 | 对应说明 |
|---|---|
root | 根元素,设置相对定位、flex 布局与瀑布流容器样式 |
item | 条目元素,设置绝对定位、宽度计算、过渡动画与瀑布流项目样式 |
对应到 API 上,classNames/styles均支持对象形式,也支持接收{ props }的函数形式(函数可在拿到最终合并后的 props 后再决定样式),并且都能通过 ConfigProvider 全局注入。官方"自定义语义结构的样式和类"示例见 components/masonry/demo/style-class.tsx,一种典型写法是:
<Masonry classNames={{ root: 'my-masonry-root', item: 'my-masonry-item', }} styles={{ root: { background: token.colorFillTertiary }, item: { borderRadius: 8 }, }} ... />当这样编写时,实际渲染结构大致为:div.root容器内,每个MasonryItem渲染为带prefixCls-item类名且绝对定位的div.item,其中prefixCls默认解析为ant-masonry(前缀由getPrefixCls('masonry', ...)产生,Masonry.tsx)。
九、布局原理:源码级的"最矮列优先"算法
理解了用法之后,值得看看 Masonry 究竟如何实现"等高列内的瀑布流"。整个布局核心集中在 usePositions.ts,算法可以用 5 步概括:
- 初始化一个长度为
columnCount、元素全为 0 的"列高数组"columnHeights; - 按 items 顺序遍历测量结果
[itemKey, itemHeight, itemColumn?]; - 若条目未显式指定
column,则选中"当前最矮的列"(columnHeights.indexOf(Math.min(...columnHeights)));若指定了,则直接使用该列号,并做Math.min(column, columnCount - 1)越界钳制; - 把该条目的
top记为columnHeights[targetColumn],随后累加columnHeights[targetColumn] += itemHeight + verticalGutter; - 最终容器总高度取
max(columnHeights) - verticalGutter(减掉最后一列多算的底部间距)。
拿到每个 item 的{ column, top }后,Masonry 在渲染层用 CSS 变量把位置落到 DOM 上(Masonry.tsx):
itemStyle = { [varName('item-width')]: `calc((100% + ${horizontalGutter}px) / ${columnCount})`, insetInlineStart: `calc(${varRef('item-width')} * ${columnIndex})`, width: `calc(${varRef('item-width')} - ${horizontalGutter}px)`, top: position.top, position: 'absolute', }即:先按"容器宽度 + 水平间距"均分得到每条目宽度,再通过insetInlineStart偏移到对应列。inset-inline语义也让组件天然兼容 RTL——样式文件在 RTL 时会追加direction: rtl的修饰类(components/masonry/style/index.ts)。
条目在列间移动或增删时的动画由两部分构成:增删使用CSSMotionList的 fade 出入场;重排时的位移过渡则由样式表对left/right/top应用motionDurationSlow的过渡实现(style/index.ts)。当列数变化(例如窗口缩放导致 4 列变 2 列)时,条目会平滑滑入新位置而不是突兀跳动。
十、Design Token 与全局配置现状
最后说明样式体系的两个现状,避免使用时的误区:
- Design Token:Masonry 文档页中 Design Token 区(
<ComponentTokenTable component="Masonry" />)目前为空,对应源码中ComponentToken也是一个空接口(components/masonry/style/index.ts),即该组件目前不提供专属 token,视觉变量沿用全局主题令牌(如motionDurationSlow、motionEaseOut等)。 - ConfigProvider 全局配置:如上文 API 表所示,只有
classNames/styles两类"语义化样式"支持通过componentConfig全局统一配置(在 Masonry 内读取键为masonry);columns、gutter、fresh、items、itemRender、onLayoutChange均只支持组件级配置。
十一、进一步阅读与测试验证
如果你想深入理解 Masonry 的行为边界,可以继续阅读当前仓库中的以下文件:
- 组件实现:components/masonry/Masonry.tsx、components/masonry/MasonryItem.tsx
- 布局与测量 hooks:components/masonry/hooks/usePositions.ts、components/masonry/hooks/useDelay.ts、components/masonry/hooks/useRefs.ts
- 样式实现:components/masonry/style/index.ts
- 官方示例(可直接复制运行):basic、responsive、image、dynamic、style-class、fresh
- 测试用例(含快照):components/masonry/tests/index.test.tsx、components/masonry/tests/semantic.test.tsx,其中语义化测试会逐项断言
root/item节点是否携带预期的 class。
组件英文文档见 components/masonry/index.en-US.md,两份文档的结构与参数保持一致。总体而言,Masonry 把"测量—分配—定位—动画"这条瀑布流核心链路封装成了声明式 API,你只需提供数据与渲染函数,列高计算、响应式切换、图片重测和位移过渡都由组件内部完成;在需要极致控制或排查布局抖动时,则可依据本文第九节的算法路径,从usePositions与尺寸收集代码入手定位问题。
【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/GitHub_Trending/an/ant-design
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考