Ant Design Empty 语义化结构定制:classNames 与 styles 对象/函数用法全解析
【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/GitHub_Trending/an/ant-design
Empty(空状态)组件是业务页面中「暂无数据」场景的标配。antd v5.23.0 起,Empty 引入了「语义化结构(Semantic DOM)」定制能力:通过classNames与styles两个属性,并支持「对象」与「函数」两种写法,即可对根节点、图片、描述、底部操作区分别做样式定制。本文以仓库中 style-class 示例 及其配套 style-class.md 说明文档为主体,结合组件源码与测试用例,完整讲解这套定制 API 的结构划分、两种写法、底层合并原理与可验证的测试行为。
Empty 的语义化结构:四个可定制节点
在动手写样式前,先明确「往哪写」。查看 Empty 组件实现 中定义的EmptySemanticType,Empty 的语义化结构一共包含四个节点:
export type EmptySemanticType = { classNames?: { root?: string; image?: string; description?: string; footer?: string; }; styles?: { root?: React.CSSProperties; image?: React.CSSProperties; description?: React.CSSProperties; footer?: React.CSSProperties; }; };这四个节点对应的实际 DOM 及默认 class 前缀(默认prefixCls为ant-empty)如下,与渲染代码一一对应:
| 语义化 key | 对应元素 | 渲染位置 | 说明(来自 Semantic DOM 交互预览) |
|---|---|---|---|
root | 最外层容器div.ant-empty | 根元素 | 控制文本对齐、字体、行高与整体布局 |
image | 图片容器div.ant-empty-image | 图片节点 | 控制高度、透明度、边距与图片样式 |
description | 描述div.ant-empty-description | 描述节点 | 控制描述文案颜色等 |
footer | 底部操作区div.ant-empty-footer | footer 节点 | 控制与描述的上边距以及操作按钮(如children传入的 Button) |
同时注意两点容易忽略的实现细节:
- 当
image使用内置的Empty.PRESENTED_IMAGE_SIMPLE(简单版小图)时,根元素会额外追加ant-empty-normalclass(见 className 拼接逻辑),可据此针对性区分默认图与简单图的样式。 - 旧的
imageStyle属性已标记为@deprecated,组件会在开发环境输出imageStyle→styles.image的废弃警告(见 deprecated 检测),新代码请统一走styles.image。
对象写法:静态为每个语义节点单独定制
在 style-class.tsx 示例中,stylesObject使用「对象」写法,为不同节点一次性注入行内样式:
const stylesObject: EmptyProps['styles'] = { root: { backgroundColor: '#f5f5f5', borderRadius: '8px' }, image: { filter: 'grayscale(100%)' }, description: { color: '#1890ff', fontWeight: 'bold' }, footer: { marginTop: '16px' }, };- 每个 key 对应上文四个语义节点之一,value 是标准的
React.CSSProperties; root的效果会落到根div.ant-empty的style上,image/description/footer同理;styles是行内样式,天然具备最高优先级,适合不需要抽离成样式文件的小范围调整。
classNames的对象写法则用于挂自定义 class(便于结合 CSS Modules、Tailwind 或普通全局样式表),示例中先借助antd-style的createStaticStyles生成带css片段的静态 class,再挂载到根节点:
import { createStaticStyles } from 'antd-style'; const classNames = createStaticStyles(({ css }) => ({ root: css` border: 1px dashed #ccc; padding: 16px; `, })); // 组件内使用 const emptyClassNames: EmptyProps['classNames'] = { root: classNames.root, }; <Empty {...emptySharedProps} description="Object styles" classNames={emptyClassNames} styles={stylesObject} />说明:示例中使用的
createStaticStyles来自antd-style(已列入仓库 package.json 依赖),用于把 CSS-in-JS 片断编译为一份可复用的静态 class;若你的项目不使用antd-style,直接在classNames.root传入自己定义的 className 字符串即可,API 语义完全一致。
示例还通过emptySharedProps复用了两个渲染实例的公共配置——image采用Empty.PRESENTED_IMAGE_SIMPLE,children传入主操作按钮Create Now——这正好演示了「语义化定制」与「内容定制」可以正交组合。
函数写法:根据 props 动态返回样式
对象写法的局限是无法感知当前组件的实际 props。因此classNames/styles都支持「函数」写法:函数接收{ props }参数(即当前 Empty 接收到的完整 props),返回同样结构的对象。示例中的stylesFn根据是否有description动态切换配色:
const stylesFn: EmptyProps['styles'] = ({ props }): GetProp<EmptyProps, 'styles', 'Return'> => { if (props.description) { return { root: { backgroundColor: '#e6f7ff', border: '1px solid #91d5ff' }, description: { color: '#1890ff', fontWeight: 'bold' }, image: { filter: 'hue-rotate(180deg)' }, }; } return {}; };写法要点:
- 入参为
{ props },props即传给<Empty>的全部属性(description、image、children等皆可参与判断); - 返回值类型与对象写法相同,且可以不写全所有节点(上例无
description时返回空对象,即「不加额外样式」); - 类型上可用
EmptyProps['styles']标注,若需精确定位函数形式返回值,可用GetProp<EmptyProps, 'styles', 'Return'>(仓库示例即采用此写法,这也是GetProp泛型工具在 v5 语义化 API 中的典型用法)。
在组件内,函数写法与对象写法可混用:classNames用对象(挂静态 class),styles用函数(动态行内样式),如示例所示。
底层原理:多来源合并与优先级
无论对象还是函数,最终都会汇入统一合并逻辑。Empty 内部调用了通用 Hook useMergeSemantic:
const [mergedClassNames, mergedStyles] = useMergeSemantic< EmptySemanticAllType['classNames'], EmptySemanticAllType['styles'], EmptyProps >([contextClassNames, classNames], [contextStyles, contextStyleRoot, styles, styleRoot], { props, });从这段实现可以得出四个关键结论:
- 函数先求值再合并:
resolveStyleOrClass会对函数形式执行value({ props }),得到结果对象后再参与合并(实现代码)。 - 支持来自
ConfigProvider的全局配置:contextClassNames/contextStyles/contextStyle通过 useComponentConfig('empty') 取自 ConfigProvider 的empty组件级配置,因此可在应用根部统一定义全局 Empty 风格,再被组件局部配置覆盖。 - 合并顺序决定优先级:styles 按传入顺序用
{ ...acc, ...cur }浅合并,后传入的键值覆盖前者(mergeStyles);classNames 则由clsx拼接共存。 - 行内 style 与全局 style 的差异被妥善处理:
style/ 上下文style会被useSemanticRootStyle包裹为{ root: style }再参与合并,保证root节点的styles.root覆盖关系符合「组件内联 > ConfigProvider」的直觉。
「root 样式优先级」这一点有专门测试背书:semantic.test.tsx 中的 root style priority 用例 通过ConfigProvider empty={{ styles, style }}与组件自身styles/style组合断言根节点最终样式,可看作该优先级的可执行规范。
测试用例佐证:函数化定制的两种形态
仓库中 semantic.test.tsx 对本示例所展示的两种写法做了直接验证:
- 函数化动态切换:
classNames/styles以函数传入时,测试断言带description时根节点挂上.empty-with-desc且背景为红,移除description后 rerender 切换为.empty-no-desc且背景为蓝——证明函数每次渲染都会基于最新 props 重新求值; - 对象形式生效:
{ root: 'empty-custom', image: 'empty-image-custom' }与{ root: {...}, image: {...} }会被正确写到对应语义节点,通过container.querySelector('.empty-custom')与.empty-image-custom断言。
同时,API 文档 中classNames/styles的类型定义也与此一一对应:
Record<SemanticDOM, string> | (info: { props }) => Record<SemanticDOM, string> // classNames Record<SemanticDOM, CSSProperties> | (info: { props }) => Record<SemanticDOM, CSSProperties> // styles两者自5.23.0版本引入,并且均可通过 ConfigProvider 的组件级全局配置 下发,实现全站空状态风格统一。
实践建议
- 静态需求用对象,条件需求用函数:仅需固定美化时对象写法更清晰;需要「有/无描述」「是否简单图」「是否 RTL」等条件分支时,用函数写法基于
props判断,避免在渲染外手工计算。 - class 与 style 分工:涉及媒体查询、伪类、动画等复杂样式优先走
classNames挂类;简单覆盖用styles行内样式更直接。 - 注意替换废弃属性:若代码中仍在使用
imageStyle,请迁移为styles.image,开发环境会收到废弃提示。 - 善用全局配置:多页面共用的空状态外观,建议收敛到
ConfigProvider的empty组件配置中,组件局部再按需微调,避免重复样板。
小结
Empty 的classNames/styles语义化定制 API,通过root、image、description、footer四个结构节点,把原本「要么不动、要么整体重写」的空状态组件拆成了可精确打击的样式面。对象与函数两种写法分别覆盖静态与动态场景,而底层的 useMergeSemantic 统一了「组件局部 > ConfigProvider 全局」的合并优先级,并以测试用例固化了行为边界。需要实际体验完整渲染效果时,可直接参考 style-class.tsx 运行示例。
【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/GitHub_Trending/an/ant-design
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考