上期分享了,Button组件的分析情况,本期进行
Tag组件
的探索;在开始之前,也同样希望能够同时把源代码打开对照阅读。这是我做的一个系列文章,我会逐渐把
Ant Design
中的组件以及其他相关内容逐步更新完,有需要可以关注这个专栏PS:我知道这种标题不带噱头的文章确实没什么流量,但是没想到流量这么差🤣;我还是本着学习的态度,会把这个系列更新完的,如果对你有用,请一键三连!
OK,那么正文开始:
一、整体架构概览
1.1 组件结构
Tag 组件采用了模块化的设计架构,主要包含以下几个核心部分:
bash
components/tag/
├── index.tsx # 主组件入口
├── CheckableTag.tsx # 可选择标签子组件
└── style/
├── index.ts # 基础样式系统
├── presetCmp.ts # 预设颜色样式组件
└── statusCmp.ts # 状态颜色样式组件
1.2 设计理念
Tag 组件的设计体现了以下几个核心理念:
- 样式分离:将不同类型的样式拆分为独立的组件
- 按需加载:只有在需要时才加载对应的样式
- 配置驱动:通过 Token 系统实现主题定制
- 类型安全:完善的 TypeScript 类型定义
二、核心实现解析
2.1 主组件架构
tag/index.tsx
文件是 Tag 组件的核心实现,分析如下:
tsx
const InternalTag = React.forwardRef<HTMLSpanElement, TagProps>((tagProps, ref) => {
// 公共配置的提取和处理
const { getPrefixCls, direction, tag: tagContext } = React.useContext(ConfigContext);
// 颜色类型判断
// 传入的color是否在预设的color中
const isPreset = isPresetColor(color);
// 传入的color是否在预设的表示状态的color中
const isStatus = isPresetStatusColor(color);
const isInternalColor = isPreset || isStatus;
// 样式计算
const tagStyle: React.CSSProperties = {
// 不在预设中的color就用传入的color当backgroundColor
backgroundColor: color && !isInternalColor ? color : undefined,
...tagContext?.style,
...style,
};
// 渲染逻辑
return wrapCSSVar(
<span className={tagClassName} style={tagStyle}>
{kids}
{mergedCloseIcon}
{isPreset && <PresetCmp key="preset" prefixCls={prefixCls} />}
{isStatus && <StatusCmp key="status" prefixCls={prefixCls} />}
</span>
);
});
关键设计亮点:
-
智能颜色处理 :通过
isPresetColor
和isPresetStatusColor
函数判断颜色类型,决定是使用内置样式还是自定义背景色 -
配置继承:支持从 ConfigProvider 继承全局配置
-
条件样式加载:只有当颜色属于预设类型时,才会渲染对应的样式组件
2.2 样式分离架构
2.2.1 预设颜色样式组件
tag/style/presetCmp.ts
实现了预设颜色的样式生成:
ts
const genPresetStyle = (token: TagToken) =>
genPresetColor(token, (colorKey, { textColor, lightBorderColor, lightColor, darkColor }) => ({
[`${token.componentCls}${token.componentCls}-${colorKey}`]: {
color: textColor,
background: lightColor,
borderColor: lightBorderColor,
// 反色模式
'&-inverse': {
color: token.colorTextLightSolid,
background: darkColor,
borderColor: darkColor,
},
},
}));
2.2.2 状态颜色样式组件
tag/style/statusCmp.ts
处理状态相关的颜色样式:
ts
const genTagStatusStyle = (
token: TagToken,
status: 'success' | 'processing' | 'error' | 'warning',
cssVariableType: CssVariableType,
): CSSInterpolation => {
return {
[`${token.componentCls}${token.componentCls}-${status}`]: {
color: token[`color${cssVariableType}`],
background: token[`color${capitalizedCssVariableType}Bg`],
borderColor: token[`color${capitalizedCssVariableType}Border`],
},
};
};
设计优势:
- 按需加载:只有使用对应颜色类型时才会加载相应样式
- 性能优化:避免了不必要的 CSS 生成
- 维护性:样式逻辑清晰分离,便于维护
2.3 Token 系统设计
tag/style/index.ts
定义了完整的 Token 系统:
ts
export interface ComponentToken {
defaultBg: string; // 默认背景色
defaultColor: string; // 默认文字颜色
}
export interface TagToken extends FullToken<'Tag'> {
tagFontSize: number;
tagLineHeight: React.CSSProperties['lineHeight'];
tagIconSize: number | string;
tagPaddingHorizontal: number;
tagBorderlessBg: string;
}
// Token 预处理
export const prepareToken: (token: Parameters<GenStyleFn<'Tag'>>[0]) => TagToken = (token) => {
const tagToken = mergeToken<TagToken>(token, {
tagFontSize: token.fontSizeSM,
tagLineHeight: unit(calc(token.lineHeightSM).mul(tagFontSize).equal()),
tagIconSize: calc(fontSizeIcon).sub(calc(lineWidth).mul(2)).equal(),
tagPaddingHorizontal: 8,
tagBorderlessBg: token.defaultBg,
});
return tagToken;
};
Token 系统特点:
- 类型安全:完整的 TypeScript 类型定义
- 计算能力:支持动态计算和响应式设计
- 主题定制:通过 Token 实现主题的完全定制
三、高级功能实现
3.1 关闭功能的设计模式
Tag 组件的关闭功能通过 components/_util/hooks/useClosable.tsx
Hook 实现,进行了配置的合并,分别是组件配置、上下文配置、默认配置:
tsx
// useClosable进行配置合并和处理
const [, mergedCloseIcon] = useClosable(
pickClosable(tagProps), // 从props中取出,closeable、closeIcon的配置(组件级)
pickClosable(tagContext), // 从props中取出,closeable、closeIcon的配置(上下文)
{
closable: false,
closeIconRender: (iconNode: React.ReactNode) => {
const replacement = (
<span className={`${prefixCls}-close-icon`} onClick={handleCloseClick}>
{iconNode}
</span>
);
return replaceElement(iconNode, replacement, (originProps) => ({
onClick: (e: React.MouseEvent<HTMLElement>) => {
originProps?.onClick?.(e);
handleCloseClick(e);
},
className: classNames(originProps?.className, `${prefixCls}-close-icon`),
}));
},
}
);
设计亮点:
- 配置优先级:Props > Context > Default 的清晰优先级
- 事件合并:优雅地合并原有事件和新增事件
- 可扩展性 :通过
closeIconRender
支持自定义渲染
3.2 CheckableTag 的独立设计
tag/CheckableTag.tsx
作为独立的子组件,实现了可选择标签的功能:
tsx
const CheckableTag = React.forwardRef<HTMLSpanElement, CheckableTagProps>((props, ref) => {
const handleClick = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
onChange?.(!checked); // 状态切换
onClick?.(e); // 原有事件
};
// prefixCls来自config-provider的prefixCls配置和props传入的prefixCls整合之后的结果,意义为统一前缀
const cls = classNames(
prefixCls,
`${prefixCls}-checkable`,
{
[`${prefixCls}-checkable-checked`]: checked,
},
// ... 其他类名
);
return wrapCSSVar(
<span
{...restProps}
ref={ref}
className={cls}
onClick={handleClick}
/>
);
});
设计特点:
- 完全受控:组件为完全受控组件,状态由外部管理
- 事件分离 :
onChange
处理状态变化,onClick
处理点击事件 - 样式复用:复用主 Tag 组件的样式系统
3.3 颜色系统的类型安全
components/_util/colors.ts
实现了完整的颜色类型系统,预设在系统中:
ts
export const PresetStatusColorTypes = [
'success', 'processing', 'error', 'default', 'warning'
] as const;
export type PresetColorType = PresetColorKey | InverseColor;
export type PresetStatusColorType = (typeof PresetStatusColorTypes)[number];
export function isPresetColor(color?: any, includeInverse = true) {
if (includeInverse) {
return [...inverseColors, ...PresetColors].includes(color);
}
return PresetColors.includes(color);
}
export function isPresetStatusColor(color?: any): color is PresetStatusColorType {
return PresetStatusColorTypes.includes(color);
}
类型安全特性:
- 编译时检查:通过 TypeScript 在编译时检查颜色类型
- 运行时验证:提供运行时的颜色类型判断函数
- 扩展性:支持反色模式和自定义颜色
四、架构设计的深层思考
4.1 组件复合模式
Tag 组件采用了组件复合模式,将 CheckableTag
作为静态属性挂载:
tsx
export type TagType = typeof InternalTag & {
CheckableTag: typeof CheckableTag;
};
const Tag = InternalTag as TagType;
Tag.CheckableTag = CheckableTag;
这种设计的优势:
- 命名空间:避免全局命名冲突
- 关联性:明确表达组件间的关系
- 便利性 :使用时更加直观
<Tag.CheckableTag />
4.2 样式架构的创新
4.2.1 CSS-in-JS 的组件化
通过将样式组件化,Ant Design 实现了第5、6行的写法:
typescript
const tagNode: React.ReactNode = (
<span {...domProps} ref={ref} className={tagClassName} style={tagStyle}>
{kids}
{mergedCloseIcon}
{isPreset && <PresetCmp key="preset" prefixCls={prefixCls} />}
{isStatus && <StatusCmp key="status" prefixCls={prefixCls} />}
</span>
);
乍一看好像PresetCmp
、StatusCmp
是占位的DOM元素,但是一个Tag
必要的元素已经集齐了,这两个是多余的,通过查看组件实现之后发现:
PresetCmp
、StatusCmp
这两个函数的实际return
的都是null
,函数中进行了样式处理的操作;也就是说:这里只是利用jsx语法来执行函数,没有产生具体的reactNode!
。
是的,这就是CSS-in-JS的组件化
,就是这个意思,听过这个概念很多次,这次看见实现原理了,有趣!
总结:这些组件实际上是样式生成函数,通过 JSX 语法调用,实现了:
- 按需加载:只有需要时才生成对应样式
- 代码分割:样式逻辑清晰分离
- 类型安全:样式生成过程的类型检查
4.2.2 Token 驱动的设计系统
Token 系统实现了设计系统的标准化:
typescript
const prepareComponentToken: GetDefaultToken<'Tag'> = (token) => ({
defaultBg: new FastColor(token.colorFillQuaternary)
.onBackground(token.colorBgContainer)
.toHexString(),
defaultColor: token.colorText,
});
通过 Token 系统:
- 一致性:确保整个设计系统的一致性
- 可定制:支持主题的深度定制
- 响应式:支持动态主题切换
Token是Ant Design组件库中非常重要的一个设计,也是一个很精妙的设计,等后续讲到了之后,详细的分析
4.3 性能优化策略
4.3.1 样式按需生成
通过条件渲染样式组件,避免不必要的 CSS 生成:
typescript
// 只有在使用预设颜色时才生成对应样式
{isPreset && <PresetCmp key="preset" prefixCls={prefixCls} />}
4.3.2 memoization 优化
在 useClosable Hook 中大量使用 useMemo
优化,useMemo是必知必会的优化手段:
typescript
const mergedClosableConfig = React.useMemo(() => {
// 复杂的配置合并逻辑
}, [propCloseConfig, contextCloseConfig, mergedFallbackCloseCollection]);
五、代码的思考
5.1 组件设计原则
从 Tag 组件的设计中,我们可以总结出以下设计原则:
- 单一职责:每个模块都有明确的职责
- 开放封闭:对扩展开放,对修改封闭
- 依赖倒置:依赖抽象而非具体实现
- 接口隔离:提供最小化的接口
组件的设计和编写,没有一句废话和废代码;每一个的逻辑和封装都是经过认真考量的,非常值得学习。
5.2 架构模式应用
- 策略模式:不同颜色类型的处理策略
- 工厂模式:样式生成的工厂函数
- 观察者模式:配置变化的响应机制
- 装饰器模式:样式的层层装饰
之前纯去学习设计模式的理论的时候,十分的枯燥,即使是给出示例代码也是很枯燥;但是看到在组件库中,真正用到了这些模式,感觉很有意思,转念一想,这样体量的代码确实是需要设计模式来支撑;建议认真去观察设计模式在这个组件的应用,受益匪浅!
5.3 工程化实践
- 类型安全:完整的 TypeScript 类型系统
- 测试覆盖:完善的单元测试和集成测试
- 文档驱动:清晰的 API 文档和使用示例
- 版本管理:向后兼容的 API 设计
这里有一点,我上期没有说,就是上面的第三点;组件库中每一个组件是承接了文档编写工作的,就是demo目录的内容;这里面的内容会被dumi使用,最终形成看到的官网文档;非常有意思!
六、总结
这一期对于Tag
的分析更加的深入,感受到了很多内容和东西;对于个人编码技术和思维的提高
非常有帮助,这一期探索下来受益匪浅。
感受最深的就是token、样式处理、CSS-in-JS组件化、useClosable
,仔细去阅读代码后,真的能感受到作者的设计和思考,非常有收获!
希望大家也去仔细学习和分析,相信你会感受到其中的精妙的!
OK,我是李仲轩,下一期再见吧👋