返回博客

组件「功能不够用」?先查命名是否混用了两个概念

做设计系统那段时间,同事拿着一张截图过来说:"这个 Badge 组件功能太少了,能不能支持角标效果?"——图上是 iOS 风格的红色计数圆点,挂在图标右上角。

我第一反应是加 prop,写一下 position="corner" 之类的 API。加了一半才发现哪里不对劲:这个 Badge 根本不是做角标用的,它就是一个内联文字 pill。两件事被塞进同一个名字里了。

现象

用户给出一张参考图:图标右上角有红色"3",头像右下角有绿色"✓"——典型的 iOS 覆盖式角标。 他们打开组件文档,只看到一堆颜色各异的文字标签(<Badge variant="success">已完成</Badge> 这类),根本做不出那个效果,于是断定「Badge 不够用」。

代码里还能找到更直接的证据:三处业务代码手写了 absolute -top-1 -right-1 来伪造角标,说明这个缺口早就存在了,只是一直靠 hack 绕过去。

根因

这个设计系统里有三个 pill 型组件:BadgeTagChip。三者外观高度相似,各自的注释说:

  • tag.tsx:"Tag = 状态标签(区别于 Badge 计数 / Chip 令牌)"
  • chip.tsx manifest:"区别 Badge 计数"

两处注释都明确说 Badge 应该是计数 / 角标,但实现里 Badge 却是个内联 pill。名字的语义已经对了,实现漂移了

根源在于设计系统早期照搬了 shadcn/ui 的 Badge(shadcn 的 Badge 是文字 pill,连角标功能都没有),而没有注意到 Ant Design / MUI 世界里 Badge 特指「覆盖式计数徽标」这个完全不同的概念。

坑:一个名字承担两类 UI 概念

Badge 同时指「inline 文字 pill」和「overlay 角标计数器」时,两种需求都无法被第一类 API 满足。用户会抱怨「功能少」,开发者会不断往组件里加 prop,最终变成一个 props 爆炸的怪物——而根本问题是概念没有分清。

成熟库的参考分界:

  • Badge(角标语义):Ant Design Badge、MUI Badge — 包裹子节点,在角落渲染计数/点/自定义内容,带 countdotplacementoffset 等 prop。
  • pill 语义:Ant Design Tag、MUI Chip、shadcn Badge(命名异类)— 内联展示状态文字,不包裹子节点。

正解

第一步:精确命名两个概念。

  • 当前 Badge 实现 = 内联 pill(应归入 Tag/Chip
  • 用户需要的 = 覆盖式角标(才是「真 Badge」)

第二步:收回名字,迁移漂移的用法。

Badge 重建为覆盖式角标组件:

// Badge — overlay count/dot/content
interface BadgeProps {
  count?: number;
  max?: number;           // 超出显示 max+
  dot?: boolean;          // 纯红点,不显示数字
  content?: ReactNode;    // 自定义内容(图标/文字)
  placement?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
  offset?: [number, number];
  ring?: boolean;         // 白色描边,分离宿主背景
  children: ReactNode;    // 被包裹的目标元素
}
 
// 用法
<Badge count={3}>
  <Icon name="bell" />
</Badge>
 
<Badge dot color="green">
  <Avatar src={avatarUrl} />
</Badge>

第三步:把原来 pill 用途全部迁移到 Tag

Tag 本就是 pill 的超集(颜色、border、closable、icon),迁移是纯视觉等价替换:

// Before(漂移的用法)
<Badge variant="success">已完成</Badge>
 
// After(语义正确)
<Tag color="success">已完成</Tag>

第四步:消灭手写角标 hack。

// Before — 业务代码手写 absolute 伪造角标
<div className="relative">
  <Icon name="message" />
  <span className="absolute -top-1 -right-1 ...">3</span>
</div>
 
// After — 一行搞定
<Badge count={3}><Icon name="message" /></Badge>
正解:检查兄弟组件的注释,名字通常已经对了

当组件「感觉不够用」时,先读同一组件库里兄弟组件的 doc-comment / manifest description。如果注释已经写了「区别于 Badge 计数」,说明库的作者早就规划好了分工,漂移的是实现,不是名字。读注释 → 确认意图 → 迁移漂移用法,比直接堆 prop 代价小得多。

一句话外卖

组件「功能不够用」时,先问自己:这个名字是否同时承担了两个不同 UI 类别?成熟库(Ant Design / MUI)的命名拆分方式是最快的分类参照。