Material UI Lab Masonry 组件完全指南:瀑布流布局的配置、实现原理与 SSR 支持
【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Google's Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui
Masonry 是 MUI Lab 中用于构建“瀑布流”布局的组件:它把宽度一致、高度不一的内容块按行顺序排布,每个新元素都会进入当前最矮的列,从而最大化利用空间。本篇基于 MUI 官方文档与@mui/lab源码,完整覆盖 Masonry 的基本用法、columns/spacing/sequential/default*等全部配置项,并深入源码剖析其基于 CSS Flexbox 负边距与order重排的布局引擎、ResizeObserver/MutationObserver的响应机制,以及服务端渲染(SSR)模式的实现细节。
一、Masonry 是什么:布局规则与适用场景
官方文档 masonry.md 对 Masonry 的定义是:
Masonry lays out contents of varying dimensions as blocks of the same width and different height with configurable gaps.(Masonry 把尺寸各异的组织内容排列为宽度相同、高度不同、间隙可配置的块。)
具体布局规则有三条:
- 等宽变高:Masonry 维护一组宽度一致、高度不同的内容块,子元素可以是任意 React 元素,包括
<div />和<img />; - 按行排序:内容按行(row)顺序进入布局。当某一行已被指定的列数填满后,下一个元素开始新行;
- 最短列优先:新元素被添加到当前最矮的列中,以此优化空间利用。
组件实现位于 Masonry.js,类型声明位于 Masonry.d.ts,测试位于 Masonry.test.js。它与另一个组件的分工值得注意:文档中明确提示,Masonry是按行(row)排序子元素的;如果你希望图片按列(column)排序,应使用ImageList的masonry布局变体。
二、基本用法:最小可用示例
BasicMasonry.tsx 展示了 Masonry 的最小形态:
import Box from '@mui/material/Box'; import Paper from '@mui/material/Paper'; import Masonry from '@mui/lab/Masonry'; import { styled } from '@mui/material/styles'; const heights = [150, 30, 90, 70, 110, 150, 130, 80, 50, 90, 100, 150, 30, 50, 80]; const Item = styled(Paper)(({ theme }) => ({ backgroundColor: '#fff', ...theme.typography.body2, padding: theme.spacing(0.5), textAlign: 'center', color: (theme.vars || theme).palette.text.secondary, })); export default function BasicMasonry() { return ( <Box sx={{ width: 500, minHeight: 393 }}> <Masonry columns={4} spacing={2}> {heights.map((height, index) => ( <Item key={index} sx={{ height }}> {index + 1} </Item> ))} </Masonry> </Box> ); }几个要点:
Masonry是容器,可接收任意数量的子元素,children是必填属性(见 Masonry.d.ts 中children: NonNullable<React.ReactNode>的声明,空 children 会触发 propTypes 警告,测试 Masonry.test.js 中有专门用例验证);- 每个子项的宽度由 Masonry 统一控制(按
columns均分容器宽度),高度由子项内容决定; - 整个布局包裹在固定宽度 500px 的
Box中,便于演示列宽分配。
三、图片瀑布流(Image Masonry)
ImageMasonry.tsx 演示了 Masonry 在图片墙场景的应用:每个子项是一个包含Label(带编号的Paper)和<img />的div,三列排布,图片通过srcSet提供 2x 高清版本并开启loading="lazy"懒加载:
<Masonry columns={3} spacing={2}> {itemData.map((item, index) => ( <div key={index}> <Label>{index + 1}</Label> <img srcSet={`${item.img}?w=162&auto=format&dpr=2 2x`} src={`${item.img}?w=162&auto=format`} alt={item.title} loading="lazy" style={{ borderBottomLeftRadius: 4, borderBottomRightRadius: 4, display: 'block', width: '100%', }} /> </div> ))} </Masonry>图片场景有一个源码层面的细节:在 Masonry.js 的handleResize中,布局引擎会检查每个子项内的嵌套<IMG>节点,如果发现某个图片的clientHeight === 0(图片尚未加载完成、没有实际渲染高度),整个重排会被标记为skip,等待图片加载后再由ResizeObserver触发重新计算。也就是说,图片异步加载完成会自动触发一次瀑布流重排,无需手动干预。
四、变高项(Items with Variable Height)
MasonryWithVariableHeightItems.tsx 用可展开的Accordion(最小高度各异)作为子项,说明 Masonry 对“动态高度”的支持:
<Masonry columns={3} spacing={2}> {heights.map((height, index) => ( <Paper key={index}> <StyledAccordion sx={{ minHeight: height }}> <AccordionSummary expandIcon={<ExpandMoreIcon />}> <Typography component="span">Accordion {index + 1}</Typography> </AccordionSummary> <AccordionDetails>Contents</AccordionDetails> </StyledAccordion> </Paper> ))} </Masonry>文档特别强调:为了满足“新元素永远进入最短列”的规则,项目之间可能在不同列之间移动。从源码结构看,这正是由 Masonry.test.js 中 “should re-compute the height of masonry when dimensions of any child change” 用例所验证的行为:当某个子项的高度从 20px 变为 10px 后,容器总高度会随之重算——因为任何子项尺寸变化都会通过ResizeObserver触发一次完整重排(见后文第五节原理分析)。
五、配置列数:columns
固定列数
FixedColumns.tsx 演示columns={4}:容器宽度被等分为 4 份,每份再减去spacing得到子项实际宽度。
响应式列数
columns接受 MUI 标准的响应式值。ResponsiveColumns.tsx 中:
<Masonry columns={{ xs: 3, sm: 4 }} spacing={2}>即小屏 3 列、sm断点及以上 4 列。从类型声明看(Masonry.d.ts),columns的类型是ResponsiveStyleValue<number | string>,因此除了对象形式{ xs, sm, md, lg, xl },还支持数组形式[3, 'sm', 4]。
源码印证:在 Masonry.js 的getStyle函数中,columns先经unstable_resolveBreakpointValues解析为各断点取值,再由handleBreakpoints为每个断点生成媒体查询样式,核心公式是子项宽度:
width = (100 / columnValue).toFixed(2) + '%'; // 子项实际宽度:calc(<width>% - <spacing>)测试文件 Masonry.test.js 的 “should generate correct responsive styles regardless of breakpoints order” 用例还验证了一个实用细节:响应式对象的键序不影响结果,传入{ sm: 5, md: 7, xs: 3 }与按断点升序排列生成完全相同的媒体查询样式。
六、配置间距:spacing
固定间距
FixedSpacing.tsx 使用spacing={3}。这里有一个关键约定(文档原文):
传入
spacing的值会被乘以主题(theme)的spacing字段。
即spacing={3}在默认主题下(spacing: (factor) => 8 * factor px)等效于 24px 的间隙。这一行为在源码中由createUnarySpacing(theme)生成的transformer实现:对数字值(以及可解析为数字的字符串)执行getValue(transformer, number),其余字符串(如'4px')则原样使用。
响应式间距
ResponsiveSpacing.tsx 中:
<Masonry columns={3} spacing={{ xs: 1, sm: 2, md: 3 }}>同样受ResponsiveStyleValue约束,可传对象或数组。
间距的实现机制值得单独说明。Masonry 并不使用 CSS gap,而是采用“容器负边距 + 子项四向半边距”的经典做法,getStyle中生成:
{ margin: `calc(0px - (${spacing} / 2))`, // 容器:抵消外溢的边距 '& > *': { margin: `calc(${spacing} / 2)` } // 每个子项:四周 spacing/2 }这样无论水平还是垂直方向,任意两个相邻子项之间的净距离都恰好是spacing。同时容器高度会在布局引擎测得maxColumnHeight(最矮列之外最高的那一列总高)后设置为maxColumnHeight + spacing,测试用例 “should apply correct default styles” 精确断言了这三个值(容器负边距-spacing/2、子项边距spacing/2、子项宽度width/columns - spacing)。
七、顺序模式:sequential
Sequential.tsx 演示:
<Masonry columns={4} spacing={2} defaultHeight={450} defaultColumns={4} defaultSpacing={1} sequential >开启sequential后,元素按从左到右的顺序依次填入各列(第 1、2、3、4 个元素分别在第 1~4 列,第 5 个元素回到第 1 列),而不是进入当前最短列。适合需要保持阅读顺序严格的场景。
源码印证:Masonry.js 的handleResize中两种策略是并列分支:
if (sequential) { columnHeights[nextOrder - 1] += childHeight; child.style.order = nextOrder; nextOrder += 1; if (nextOrder > currentNumberOfColumns) { nextOrder = 1; } } else { const currentMinColumnIndex = columnHeights.indexOf(Math.min(...columnHeights)); columnHeights[currentMinColumnIndex] += childHeight; child.style.order = currentMinColumnIndex + 1; }测试用例 “should place children in sequential order”(浏览器环境)验证了 2 列 3 个子项时计算样式 order 依次为1, 2, 1。
八、服务端渲染(SSR):defaultHeight/defaultColumns/defaultSpacing
Masonry 的动态布局依赖浏览器 API(读取子项实际高度),在服务端执行时这些值不可用。为此组件提供三个仅用于 SSR 的 prop(见 SSRMasonry.tsx):
<Masonry columns={4} spacing={2} defaultHeight={450} defaultColumns={4} defaultSpacing={1} >文档中的注意事项原文:
defaultHeight应当足够大以容纳所有行。另外需要注意,在服务端渲染的情况下,元素不会被添加到最短列。
进入 SSR 分支的条件(Masonry.js L189-L196):
const isSSR = !maxColumnHeight && defaultHeight && defaultColumns !== undefined && defaultSpacing !== undefined;即首次服务端渲染(尚未测得maxColumnHeight)且三个default*prop 全部提供时启用。SSR 分支生成的纯 CSS 样式为(L52-L77):
- 容器高度固定为
defaultHeight(px); - 容器与子项使用
defaultSpacing换算出的固定负/正半边距; - 每个子项宽度为
calc(100/defaultColumns% - defaultSpacing); - 用
nth-of-type(defaultColumns n + i)选择器为子项依次赋予order: 1..defaultColumns,实现“按行左到右”的确定性列分配——这正是文档所说“SSR 下不进入最短列”的原因:纯 CSS 无法知道每项高度,只能做静态轮转。
浏览器端水合(hydrate)完成后,handleResize会立即运行,用真实测量值接管布局,SSR 静态样式随之被动态order与容器高度覆盖。测试用例 “should support server-side rendering” 对isSSR: true时的完整样式对象(含nth-of-type(4n+1)→order: 1等规则)做了精确断言。
九、布局引擎原理:从源码看 Masonry 如何工作
结合 Masonry.js 全文,Masonry 的运行时机制可以归纳为四部分:
1. 静态骨架:Flexbox 容器
getStyle生成的基础容器样式是:
{ width: '100%', display: 'flex', flexFlow: 'column wrap', // 纵向主轴 + 允许换行 alignContent: 'flex-start', boxSizing: 'border-box', }注意主轴方向是column:每个“行”是 Flex 容器的一条主轴,换行产生下一行。子项通过order属性决定落入哪一列,从而把二维瀑布流映射为“纵向 Flex + order 重排”的一维问题。
2. 防合并哨兵:line breaks
源码 L353-L358 揭示了一个精妙设计——组件末尾渲染一组透明哨兵元素:
// A line break is added to the end of each column to prevent columns from merging. const lineBreaks = new Array(numberOfLineBreaks).fill('').map((_, index) => ( <span key={index} contenteditable="false">【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Google's Material Design. Free forever.
项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考