「Ant Design 组件库探索」二:Tag组件

上期分享了,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>
  );
});

关键设计亮点:

  1. 智能颜色处理 :通过 isPresetColorisPresetStatusColor 函数判断颜色类型,决定是使用内置样式还是自定义背景色

  2. 配置继承:支持从 ConfigProvider 继承全局配置

  3. 条件样式加载:只有当颜色属于预设类型时,才会渲染对应的样式组件

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`],
    },
  };
};

设计优势:

  1. 按需加载:只有使用对应颜色类型时才会加载相应样式
  2. 性能优化:避免了不必要的 CSS 生成
  3. 维护性:样式逻辑清晰分离,便于维护

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 系统特点:

  1. 类型安全:完整的 TypeScript 类型定义
  2. 计算能力:支持动态计算和响应式设计
  3. 主题定制:通过 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`),
      }));
    },
  }
);

设计亮点:

  1. 配置优先级:Props > Context > Default 的清晰优先级
  2. 事件合并:优雅地合并原有事件和新增事件
  3. 可扩展性 :通过 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}
    />
  );
});

设计特点:

  1. 完全受控:组件为完全受控组件,状态由外部管理
  2. 事件分离onChange 处理状态变化,onClick 处理点击事件
  3. 样式复用:复用主 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);
}

类型安全特性:

  1. 编译时检查:通过 TypeScript 在编译时检查颜色类型
  2. 运行时验证:提供运行时的颜色类型判断函数
  3. 扩展性:支持反色模式和自定义颜色

四、架构设计的深层思考

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>
  );

乍一看好像PresetCmpStatusCmp是占位的DOM元素,但是一个Tag必要的元素已经集齐了,这两个是多余的,通过查看组件实现之后发现:

PresetCmpStatusCmp这两个函数的实际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 组件的设计中,我们可以总结出以下设计原则:

  1. 单一职责:每个模块都有明确的职责
  2. 开放封闭:对扩展开放,对修改封闭
  3. 依赖倒置:依赖抽象而非具体实现
  4. 接口隔离:提供最小化的接口

组件的设计和编写,没有一句废话和废代码;每一个的逻辑和封装都是经过认真考量的,非常值得学习。

5.2 架构模式应用

  1. 策略模式:不同颜色类型的处理策略
  2. 工厂模式:样式生成的工厂函数
  3. 观察者模式:配置变化的响应机制
  4. 装饰器模式:样式的层层装饰

之前纯去学习设计模式的理论的时候,十分的枯燥,即使是给出示例代码也是很枯燥;但是看到在组件库中,真正用到了这些模式,感觉很有意思,转念一想,这样体量的代码确实是需要设计模式来支撑;建议认真去观察设计模式在这个组件的应用,受益匪浅!

5.3 工程化实践

  1. 类型安全:完整的 TypeScript 类型系统
  2. 测试覆盖:完善的单元测试和集成测试
  3. 文档驱动:清晰的 API 文档和使用示例
  4. 版本管理:向后兼容的 API 设计

这里有一点,我上期没有说,就是上面的第三点;组件库中每一个组件是承接了文档编写工作的,就是demo目录的内容;这里面的内容会被dumi使用,最终形成看到的官网文档;非常有意思!

六、总结

这一期对于Tag的分析更加的深入,感受到了很多内容和东西;对于个人编码技术和思维的提高非常有帮助,这一期探索下来受益匪浅

感受最深的就是token、样式处理、CSS-in-JS组件化、useClosable,仔细去阅读代码后,真的能感受到作者的设计和思考,非常有收获!

希望大家也去仔细学习和分析,相信你会感受到其中的精妙的!

OK,我是李仲轩,下一期再见吧👋

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax