首页
/ Ant Design Menu 语义化结构定制实战:classNames 与 styles 的对象/函数用法全解析

Ant Design Menu 语义化结构定制实战:classNames 与 styles 的对象/函数用法全解析

2026-09-07 16:29:25作者:庞眉杨Will

导读

在 Ant Design v6 中,Menu 提供了 classNamesstyles 两个高阶定制入口,允许开发者通过对象或函数两种形态,精准控制菜单内部各语义化结构(Semantic DOM)节点的 class 与行内样式。本文以仓库中 style-class 示例 为主体,结合 Menu API 文档Semantic DOM 演示,完整拆解 Menu 语义化结构的键名体系、两种传参形态的写法差异,以及"对象做静态定制、函数做条件化定制"的实战套路。

一、先理解目标:Menu 的 Semantic DOM 结构

classNames / styles 定制的对象并非随意命名的 class,而是 Menu 组件内部预定义的语义化结构节点。仓库中用于文档演示的 _semantic.tsx 完整列出了这些键名及其含义(中文描述取自该文件 locales.cn):

语义键 对应节点与职责
root 根元素,包含菜单容器的基础样式和布局
item 条目元素,包含相对定位、块级显示、外边距、空白符处理、光标样式、过渡动画等菜单项基础交互样式
itemContent 条目内容元素,包含菜单项内容的布局与排版
itemIcon 图标元素,包含最小宽度、字体大小、过渡动画、图标重置样式以及与文本的间距控制
itemTitle 菜单标题元素(horizontal 模式不生效),包含标题文字的样式与布局
list 菜单列表元素(horizontal 模式不生效),包含列表的布局与容器样式
popup 弹出菜单(inline 模式不生效),包含弹出层的定位、层级、背景等样式
subMenu.itemTitle 子菜单标题元素
subMenu.list 子菜单列表元素
subMenu.item 子菜单单项元素
subMenu.itemIcon 子菜单条目图标元素
subMenu.itemContent 子菜单条目内容元素

从键名可以看出两个要点:

  1. 支持嵌套写法:形如 subMenu.itemTitle 的点分键,说明语义结构可以深入到子菜单内部,而不是只能改外层容器;
  2. 受模式约束horizontal(水平)与 inline(内嵌)两种 mode 下,部分节点并不渲染——例如 listitemTitle 只在非 horizontal 模式存在,而 popup 只在非 inline 模式出现。这解释了 _semantic.tsxmode !== 'inline' / mode !== 'horizontal' 的动态键名拼接逻辑。

二、API 形态:对象 or 函数

依据 Menu API 文档,两个 API 的类型签名相同,只是键值类型不同:

参数 说明 类型
classNames 用于自定义组件内部各语义化结构的 class Record<SemanticDOM, string> | (info: { props }) => Record<SemanticDOM, string>
styles 用于自定义组件内部各语义化结构的行内 style Record<SemanticDOM, CSSProperties> | (info: { props }) => Record<SemanticDOM, CSSProperties>

两者都支持两种传参形态

  • 对象形态Record<语义键, 样式值>,静态地给每个结构节点写死样式,适合写法固定的定制需求;
  • 函数形态(info: { props }) => Record<语义键, 样式值>,在渲染时接收 Menu 当前收到的 props,据此条件化产出样式表,适合同一份 Menu 在不同场景下要切换样式的需求。

classNames 的值是 class 字符串,styles 的值是 CSSProperties 对象——前者利于复用全局样式文件中的类名,后者适合直接内联书写、无需维护额外样式文件。在仓库 demo/style-class.tsx 的 API 注释与文档的代码演示标记中,这一能力对应版本为 6.0.0(<code src="./demo/style-class.tsx" version="6.0.0">),即使用前请确认组件版本 ≥ 6.0.0。

三、对象形态:classNames + styles 静态定制

style-class.tsxApp 中第一个 Menu 组合了三种对象式写法,是静态定制的标准示范:

import React from 'react';
import { Flex, Menu } from 'antd';
import type { GetProp, MenuProps } from 'antd';
import { createStaticStyles } from 'antd-style';

const classNames = createStaticStyles(({ css }) => ({
  root: css`
    border: 1px solid #f0f0f0;
    max-width: 600px;
    padding: 8px;
    border-radius: 4px;
  `,
  item: css`
    color: #1677ff;
  `,
}));

const styles: MenuProps['styles'] = {
  root: { border: '1px solid #f0f0f0', padding: 8, borderRadius: 4 },
  item: { color: '#1677ff' },
  subMenu: { list: { color: '#fa541c' } },
};

3.1 classNames 的用法细节

classNames 对象中直接以语义键命名:root 用于给整个菜单容器加边框、圆角与最大宽度;item 用于统一把菜单项文字改为品牌蓝 #1677ff

这里用到的 createStaticStyles 来自 antd-style 库(示例中直接 import { createStaticStyles } from 'antd-style'),其回调内接收 css 模板函数,返回以语义键为 key 的静态 CSS 类名集合。这样做的好处是:

  • 样式以 CSS 语法书写(含嵌套、伪类、媒体查询),表达力强于行内对象;
  • 类名在模块层面静态生成、全局唯一,不会随组件重复实例化而膨胀。

若项目不便引入 antd-style,直接传入普通字符串类名同样成立:

const classNames = {
  root: 'my-menu-root',
  item: 'my-menu-item',
};
// 配合全局样式表:
// .my-menu-root { border: 1px solid #f0f0f0; border-radius: 4px; }

3.2 styles 的嵌套键写法

classNames 不同,styles 除了顶层语义键(rootitem),还支持对象嵌套形式来表达点分键,例如示例中的:

subMenu: { list: { color: '#fa541c' } },

等价于给 subMenu.list(子菜单的列表容器)写入 color: '#fa541c'。当结构层级较多、希望按树形组织时,这种嵌套对象比 'subMenu.list': { ... } 的扁平点分键可读性更强——二者在语义键层面指向同一个节点。示例同时也展示了 root / item 键可直接用 CSSProperties 内联声明,非常适合做容器描边、间距、颜色这类轻量视觉微调。

四、函数形态:依据 props 动态产出样式

静态对象无法感知 Menu 实例的差异。函数形态解决的就是这个问题——渲染时拿到 Menu 当前的 props,再决定返回哪些样式:

const stylesFn: MenuProps['styles'] = (info): GetProp<MenuProps, 'styles', 'Return'> => {
  const hasSub = info.props.items?.[0];
  return {
    root: {
      backgroundColor: hasSub ? 'rgba(240,249,255, 0.6)' : 'rgba(255,255,255)',
    },
  };
};

回调签名的入参是 { props },其中 props 即当前 Menu 组件收到的属性集合。上述逻辑先读取 info.props.items 的首项,判断其是否携带 children(即是否包含子菜单结构),据此为 root 选择浅蓝或纯白的背景。

在示例 App 中,函数形态被挂在 mode="inline" 的第二个 Menu 上:

const App: React.FC = () => {
  const shareProps: MenuProps = {
    classNames,
    items,
  };

  return (
    <Flex vertical gap="medium">
      <Menu {...shareProps} styles={styles} />
      <Menu mode="inline" {...shareProps} styles={stylesFn} />
    </Flex>
  );
};

注意两个 Menu 共享同一份 items,因此 hasSub 在两个实例中计算结果一致——这里的函数形态展示的是机制本身:一旦多个 Menu 实例的数据源(或 mode、selectedKeys 等 props)不同,函数就能各自返回差异化样式,例如"无子菜单的扁平导航背景加白、有分组/子菜单的导航使用品牌色浅底"。若所有实例配置完全相同,函数与静态对象并无差别,择优使用简单形态即可。

对应的数据类型同样需要精确标注,示例中借助 GetProp<MenuProps, 'styles', 'Return'>MenuProps['styles'] 反推函数返回值类型,保证键名与 CSSProperties 均受类型约束,避免手写 Record 导致拼错语义键。

五、对象与函数的选型建议与边界提醒

综合仓库示例与 API 文档,可归纳以下实践结论:

  1. 能静态则静态,需分支再上函数:固定的描边、内边距、圆角、统一文字色,直接对象写法;只有样式依赖运行时 props(如是否含子菜单、当前 mode)时,才把 styles / classNames 写成函数。
  2. classNames 与 styles 可叠加使用:示例中同一 Menu 同时传入 classNamesstyles(对象),二者分别落为 class 与内联样式,互不覆盖、共同生效。class 适合承载整套视觉体系,inline style 适合承载单点微调。
  3. 注意 mode 相关的空节点horizontal 模式下不存在 listitemTitleinline 模式下不存在 popup。对不存在的节点写入样式不会报错但也不会产生视觉影响,因此定制前应结合目标 mode 挑选有效语义键。
  4. 函数形态的类型保持:以函数形式声明时,建议像示例一样显式标注返回值类型,避免弱类型对象导致 TS 丢失语义键与 CSSProperties 的联合校验。
  5. 版本前提:该定制能力在仓库文档标注为 6.0.0(demo 亦标记 version="6.0.0"),低于此版本请退回传统 className / style + 全局样式覆盖方案。

六、延伸:去文档与源码中继续深挖

  • 完整可运行示例见 components/menu/demo/style-class.tsx,其中 classNamesstylesstylesFn 三种形态集中在同一文件,适合对照阅读;
  • 语义结构键名的交互式可视化(含模式切换 Segmented)见 components/menu/demo/_semantic.tsx,中文注释可直接对应到上文键名表;
  • API 类型签名与版本说明见 components/menu/index.zh-CN.md(英文版对应 index.en-US.md),其中 ItemTypeMenuItemType 等数据结构会决定 items 里哪些字段可被函数形态读取;
  • 若关心语义化结构 API 在更多组件上的通用约定,可进一步查看仓库其他组件的 demo/*-class.tsx 类示例与各自文档的 "Semantic DOM" 小节,用法与 Menu 一致:对象/函数 + 预定义语义键。

整体而言,Menu 的 classNames / styles 把"精细定制组件内部节点"从"依赖内部 DOM 结构的脆弱选择器"升级为"稳定、声明式、类型安全"的公共 API。掌握对象与函数两种形态,即可在保持组件内部实现可升级的前提下,自由控制导航菜单每一层结构的视觉表现。

登录后查看全文
热门项目推荐
相关项目推荐