上期分享了,Ant Design的工程化结构内容,本期就从组件库的第一个组件Button开始,来进行Button组件的研究和学习;在开始之前,也同样希望能够同时把源代码打开对照阅读。OK,那么正文开始:
一、Button 组件概览
在开始深入分析之前,我们先来看一下 Button 组件的整体结构。Ant Design 的 Button 组件主要由以下几个文件组成:
button.tsx
:Button 组件的核心实现button-group.tsx
:ButtonGroup 组件的实现buttonHelpers.tsx
:辅助函数和类型定义DefaultLoadingIcon.tsx
:加载图标的实现IconWrapper.tsx
:图标包装器style/index.ts
:样式定义
这种文件组织结构非常清晰,每个文件都有明确的职责,这也是大型组件库代码组织的一个典型范例。
二、Button 组件的核心实现
2.1 组件接口设计
首先,我们来看 Button 组件的接口设计。在 button.tsx
中,定义了 BaseButtonProps
和 ButtonProps
两个接口:
tsx
export interface BaseButtonProps {
type?: ButtonType;
color?: ButtonColorType;
variant?: ButtonVariantType;
icon?: React.ReactNode;
iconPosition?: 'start' | 'end';
shape?: ButtonShape;
size?: SizeType;
disabled?: boolean;
loading?: boolean | { delay?: number; icon?: React.ReactNode };
prefixCls?: string;
className?: string;
rootClassName?: string;
ghost?: boolean;
danger?: boolean;
block?: boolean;
children?: React.ReactNode;
[key: `data-${string}`]: string;
classNames?: { icon: string };
styles?: { icon: React.CSSProperties };
}
export interface ButtonProps extends BaseButtonProps, MergedHTMLAttributes {
href?: string;
htmlType?: ButtonHTMLType;
autoInsertSpace?: boolean;
}
这种接口设计有几个值得注意的点:
- 将基础属性和 HTML 属性分开定义,然后通过继承合并,这样可以更清晰地管理属性
- 使用类型别名和联合类型来限制属性的可选值,提高类型安全性
- 支持
data-*
属性的动态传递,增强了组件的扩展性
2.2 组件实现
Button 组件的核心实现是一个 React.forwardRef 包装的函数组件:
tsx
const InternalCompoundedButton = React.forwardRef<
HTMLButtonElement | HTMLAnchorElement,
ButtonProps
>((props, ref) => {
// 组件实现...
});
使用 forwardRef 可以将 ref 转发到实际的 DOM 元素,这对于需要直接操作 DOM 的场景非常有用。
2.3 状态管理
Button 组件内部使用了多个 useState 和 useEffect 钩子来管理状态:
tsx
const [innerLoading, setLoading] = useState<boolean>(loadingOrDelay.loading);
const [hasTwoCNChar, setHasTwoCNChar] = useState<boolean>(false);
特别是 loading 状态的处理非常精细:
tsx
useEffect(() => {
let delayTimer: ReturnType<typeof setTimeout> | null = null;
if (loadingOrDelay.delay > 0) {
delayTimer = setTimeout(() => {
delayTimer = null;
setLoading(true);
}, loadingOrDelay.delay);
} else {
setLoading(loadingOrDelay.loading);
}
function cleanupTimer() {
if (delayTimer) {
clearTimeout(delayTimer);
delayTimer = null;
}
}
return cleanupTimer;
}, [loadingOrDelay]);
这段代码实现了延迟加载的功能,当 loading.delay
大于 0 时,会延迟显示加载状态,这可以避免闪烁问题,提升用户体验。同时,在组件卸载时会清理定时器,防止内存泄漏。
三、Button 的样式处理
3.1 样式生成
Ant Design 使用了 CSS-in-JS 的方式来处理样式,Button 组件的样式定义在 style/index.ts
中:
tsx
const useStyle = genComponentStyleHook('Button', (token) => {
const buttonToken = mergeToken<ButtonToken>(token, {
buttonPaddingHorizontal: token.paddingSM,
buttonPaddingVertical: Math.max(
0,
(token.controlHeight - token.fontSize * token.lineHeight) / 2 - token.lineWidth,
),
buttonPaddingHorizontalSM: token.paddingXS,
buttonPaddingVerticalSM: Math.max(
0,
(token.controlHeightSM - token.fontSize * token.lineHeight) / 2 - token.lineWidth,
),
buttonPaddingHorizontalLG: token.padding,
buttonPaddingVerticalLG: Math.max(
0,
(token.controlHeightLG - token.fontSizeLG * token.lineHeight) / 2 - token.lineWidth,
),
buttonIconOnlyWidth: token.controlHeight - token.lineWidth * 2,
buttonIconOnlySizeSM: token.controlHeightSM - token.lineWidth * 2,
buttonIconOnlySizeLG: token.controlHeightLG - token.lineWidth * 2,
buttonBlockBorderWidth: 0,
});
return [
genSharedButtonStyle(buttonToken),
genButtonColorStyle(buttonToken),
genButtonSizeStyle(buttonToken),
genGroupStyle(buttonToken),
];
});
这种方式有几个优点:
- 样式与组件紧密结合,便于维护
- 支持主题定制和样式变量
- 可以根据不同的条件生成不同的样式
3.2 类名生成
Button 组件使用 classNames 库来生成类名:
tsx
const classes = classNames(
prefixCls,
hashId,
cssVarCls,
{
[`${prefixCls}-${shape}`]: shape !== 'default' && shape,
[`${prefixCls}-${mergedType}`]: mergedType,
[`${prefixCls}-dangerous`]: danger,
[`${prefixCls}-color-${mergedColorText}`]: mergedColorText,
[`${prefixCls}-variant-${mergedVariant}`]: mergedVariant,
[`${prefixCls}-${sizeCls}`]: sizeCls,
[`${prefixCls}-icon-only`]: !children && children !== 0 && !!iconType,
[`${prefixCls}-background-ghost`]: ghost && !isUnBorderedButtonVariant(mergedVariant),
[`${prefixCls}-loading`]: innerLoading,
[`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && mergedInsertSpace && !innerLoading,
[`${prefixCls}-block`]: block,
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-icon-end`]: iconPosition === 'end',
},
compactItemClassnames,
className,
rootClassName,
contextClassName,
);
这种方式可以根据组件的不同状态和属性生成对应的类名,非常灵活。
四、Button 的特殊处理
4.1 中文字符间距
Ant Design 的 Button 组件有一个特殊的功能:自动在两个中文字符之间插入空格,以提升视觉效果。这是通过 isTwoCNChar
和 spaceChildren
两个函数实现的:
tsx
// 检测是否为两个中文字符
const rxTwoCNChar = /^[\u4E00-\u9FA5]{2}$/;
export const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar);
// 在子元素之间插入空格
export function spaceChildren(children: React.ReactNode, needInserted: boolean) {
let isPrevChildPure = false;
const childList: React.ReactNode[] = [];
React.Children.forEach(children, (child) => {
const type = typeof child;
const isCurrentChildPure = type === 'string' || type === 'number';
if (isPrevChildPure && isCurrentChildPure) {
const lastIndex = childList.length - 1;
const lastChild = childList[lastIndex];
childList[lastIndex] = `${lastChild}${child}`;
} else {
childList.push(child);
}
isPrevChildPure = isCurrentChildPure;
});
return React.Children.map(childList, (child) =>
splitCNCharsBySpace(child as React.ReactElement | string | number, needInserted),
);
}
这个功能看似小,但体现了 Ant Design 对细节的极致追求。
4.2 Wave 效果
Button 组件还集成了 Wave 效果,当用户点击按钮时会有一个波纹动画:
tsx
if (!isUnBorderedButtonVariant(mergedVariant)) {
buttonNode = (
<Wave component="Button" disabled={innerLoading}>
{buttonNode}
</Wave>
);
}
这个效果增强了用户的交互体验,让用户能够明确感知到点击行为。
五、ButtonGroup 组件(已被弃用)
ButtonGroup 组件用于将多个按钮组合在一起,形成一个按钮组:
tsx
const ButtonGroup: React.FC<ButtonGroupProps> = (props) => {
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const { prefixCls: customizePrefixCls, size, className, ...others } = props;
const prefixCls = getPrefixCls('btn-group', customizePrefixCls);
const [, , hashId] = useToken();
const sizeCls = React.useMemo<string>(() => {
switch (size) {
case 'large':
return 'lg';
case 'small':
return 'sm';
default:
return '';
}
}, [size]);
// 警告处理...
const classes = classNames(
prefixCls,
{
[`${prefixCls}-${sizeCls}`]: sizeCls,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
hashId,
);
return (
<GroupSizeContext.Provider value={size}>
<div {...others} className={classes} />
</GroupSizeContext.Provider>
);
}
这个组件的实现相对简单,主要是提供了一个 Context 来共享 size 属性,并应用了相应的样式。
六、代码格式
如果你去打开了源码的Button.tsx
文件进行查看,你会发现,文件中的代码写得非常的规范,无论是注释,还是代码分块逻辑,都是非常清晰的,非常有助于代码的可读性,如下所示:

这样的代码风格非常值得学习。
除此之外,还有一个感受就是要看一个东西在哪里,是需要层层地点进去,代码的封装程度非常高,举个例子:
比如这个引入import omit from 'rc-util/lib/omit';
,这个函数的用处是在这里:
tsx
const linkButtonRestProps = omit(rest as ButtonProps & { navigate: any }, ['navigate']);
这里的作用是克隆第一个参数,把第二个参数中的属性去掉之后返回。
源文件其实就是这样的代码:
js
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = omit;
function omit(obj, fields) {
var clone = Object.assign({}, obj);
if (Array.isArray(fields)) {
fields.forEach(function (key) {
delete clone[key];
});
}
return clone;
}
这只是一个短函数,可以直接定义在当前文件中;但是这里的处理还是把它提出去了,体现了组件库对于封装程度的高要求。
七、联动
组件中除了自由属性之外,还要考虑到被上层受控的情况,尤其是和主题、表单内嵌用法相关的情况,所以可以在代码里面看到非常多的来自上层的context,要对上层的属性进行处理,判断是否被上层的属性控制。类似于以下的引入:
tsx
import { ConfigContext, useComponentConfig } from '../config-provider/context';
import DisabledContext from '../config-provider/DisabledContext';
import useSize from '../config-provider/hooks/useSize';
import type { SizeType } from '../config-provider/SizeContext';
只从这一个组件看不出来,整体的context的布局和处理,等到后续理清楚之后,我会进行画图说明,把里面的全局变量的架构讲清楚
八、开发阶段的处理
在源码中,有这样的两个对于开发环境的处理:
tsx
// ========================== Warn ==========================
if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('Button');
warning(
!(typeof icon === 'string' && icon.length > 2),
'breaking',
`\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`,
);
warning(
!(ghost && isUnBorderedButtonVariant(mergedVariant)),
'usage',
"`link` or `text` button can't be a `ghost` button.",
);
}
这里是对两个使用情况进行警告提示⚠️
tsx
if (process.env.NODE_ENV !== 'production') {
Button.displayName = 'Button';
}
这里的是为了提供更加清晰的组件名,displayName 属性主要用于调试目的,它会在 React DevTools 中显示组件的名称
九、总结
通过对 Ant Design Button 组件的分析,我们可以学习到以下几点:
- 组件接口设计:清晰的接口定义和类型约束,提高了组件的可用性和类型安全性
- 状态管理:合理使用 React Hooks 管理组件状态,处理复杂的交互逻辑
- 样式处理:采用 CSS-in-JS 方式,实现了样式与组件的紧密结合和主题定制
- 细节处理:如中文字符间距、Wave 效果等细节处理,提升了用户体验
- 组件组合:通过 ButtonGroup 组件,实现了组件的组合使用
- 代码规范:代码书写十分规范,注释清晰明朗,高度封装
- 主题一致:通过context接收上层控制信息,处理受控情况,保留主题一致性
- 开发阶段处理:对某些用法进行了警告提示以及为了配合ReactTools等工具,对开发阶段的displayName做了加强
总结一下就是:站在组件库的角度来处理组件,需要考虑得更多和更全面。 这些设计和实现思路不仅适用于 Button 组件,也可以应用到其他组件的开发中。
希望这篇文章能对大家有所帮助,让我们在组件开发中能够更加得心应手。
如果你有任何问题或想法,欢迎在评论区留言讨论!
我是李仲轩,下一篇再见👋!