「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,我是李仲轩,下一期再见吧👋

相关推荐
星之金币1 分钟前
关于我用Cursor优化了一篇文章:30 分钟学会定制属于你的编程语言
前端·javascript
天外来物2 分钟前
实战分享:用CI/CD实现持续部署
前端·nginx·docker
moxiaoran57534 分钟前
Spring Cloud Gateway 动态路由实现方案
运维·服务器·前端
市民中心的蟋蟀4 分钟前
第十一章 这三个全局状态管理库之间的共性与差异 【上】
前端·javascript·react.js
vvilkim18 分钟前
Flutter 常用组件详解:Text、Button、Image、ListView 和 GridView
前端·flutter
vvilkim24 分钟前
Flutter 命名路由与参数传递完全指南
前端·flutter
NA25 分钟前
redis
前端
你真好看_25 分钟前
6年低代码 零代码 系统二开人员的角度,看低代码 到底有多好用!!!
前端
JC_You_Know32 分钟前
边缘计算一:现代前端架构演进图谱 —— 从 SPA 到边缘渲染
前端·人工智能·边缘计算
DoraBigHead35 分钟前
深入 JavaScript 作用域机制:透视 V8 引擎背后的执行秘密
前端·javascript