React 组件库与设计系统:从基础组件到业务组件层
把 React 写成一堆散组件不难,难的是把它们抽象成一套能长期维护的组件库和设计系统。
这里从几个层次往下拆:基础组件、组合组件、业务组件,以及它们和设计规范之间的关系。
分层:Basic / Composite / Business
可以先把组件大致分成三层:
基础组件(Basic)
- Button / Input / Select / Modal / Tabs / Popover 等;
- 对应设计系统里的最小交互单元和基础 pattern。
组合组件(Composite)
- 由多个基础组件拼出来的通用结构,比如表单布局、搜索栏、数据表格(不含业务字段)、步骤条等;
- 强调整体行为和布局,而不是具体字段。
业务组件(Business)
- 直接绑定某个业务领域,例如「订单列表表格」「权限配置面板」「审批流编辑器」;
- 对特定业务字段/流程有强依赖。
分清这三层之后,一个直接的好处是:
当需求变化时,先判断这是设计规范变化,还是业务规则变化,再决定动哪一层。
基础组件:贴近设计 Token
基础组件尽量对齐设计系统中的 Token(颜色、间距、圆角、字号等),而不是在组件内写死各种 magic number。
1type ButtonVariant = 'primary' | 'secondary' | 'ghost';
2type ButtonSize = 'sm' | 'md' | 'lg';
3
4type ButtonProps = {
5 variant?: ButtonVariant;
6 size?: ButtonSize;
7} & React.ButtonHTMLAttributes<HTMLButtonElement>;
8
9function Button({ variant = 'primary', size = 'md', className, ...rest }: ButtonProps) {
10 const classes = ['btn', `btn-${variant}`, `btn-${size}`, className].filter(Boolean).join(' ');
11
12 return <button className={classes} {...rest} />;
13}
这里的 variant / size 对应的应该是设计系统里已经约定好的几种按钮风格。
一旦设计规范发生调整,只需要在样式层面更新对应 Token,组件 API 可以保持稳定。
组合组件:固化交互模式
组合组件主要工作是把基础组件按某一种高频交互模式组织起来:
- 搜索栏:Input + Select + Button + 条件布局;
- 表单布局:多列栅格、表单项标签、错误展示的统一规则;
- 数据表格:分页、排序、筛选、批量操作等。
例如一个通用搜索栏:
1type SearchBarProps = {
2 value: string;
3 placeholder?: string;
4 onChange: (value: string) => void;
5 onSearch: () => void;
6 extra?: React.ReactNode;
7};
8
9function SearchBar({ value, placeholder, onChange, onSearch, extra }: SearchBarProps) {
10 return (
11 <div className="search-bar">
12 <input
13 value={value}
14 placeholder={placeholder}
15 onChange={e => onChange(e.target.value)}
16 />
17 <button onClick={onSearch}>搜索</button>
18 {extra && <div className="search-extra">{extra}</div>}
19 </div>
20 );
21}
业务方只需要往 extra 里塞筛选项,就能复用「搜索栏长什么样、按钮放哪儿、间距如何」这些约定。
业务组件:贴业务,不贴基础设施
业务组件不可避免要理解具体领域概念,但可以尽量做到:
- API 围绕业务对象设计,而不是围绕某个 UI 库;
- 避免直接暴露第三方库的细节(比如直接把 antd 的 props 全挂上来)。
例如一个「订单列表」组件:
1type Order = {
2 id: string;
3 status: 'pending' | 'paid' | 'cancelled';
4 amount: number;
5 createdAt: string;
6};
7
8type OrderListProps = {
9 orders: Order[];
10 onViewDetail?: (id: string) => void;
11};
12
13function OrderList({ orders, onViewDetail }: OrderListProps) {
14 // 内部可以用任意表格实现,但对外只暴露 Order 相关的 API
15}
这样,即使未来从某个 UI 框架迁移到另一个,业务组件的使用方感知到的变化也尽量少。
主题与暗色模式
设计系统里经常会有多主题或暗色模式需求。
比较常见的做法是:
- 在样式层定义一套 Token(通过 CSS 变量、SASS 变量等);
- 用顶层的
<ThemeProvider>或className切换主题; - 组件内部尽量只引用 Token,而不是硬编码颜色值。
例如:
1// 全局样式里
2:root {
3 --color-bg: #ffffff;
4 --color-text: #222222;
5}
6
7[data-theme='dark'] {
8 --color-bg: #121212;
9 --color-text: #f5f5f5;
10}
组件里只关心变量:
1function Card({ children }: { children: React.ReactNode }) {
2 return <div className="card">{children}</div>;
3}
切换主题时,只需要在顶层切换 data-theme 即可:
1document.documentElement.dataset.theme = 'dark';
可访问性(a11y)在组件库里的落地
组件库一旦要被广泛复用,可访问性最好在组件层就考虑进去,而不是留给每个业务页面自己填坑。
典型的工作包括:
- 对话框:管理好 ARIA 属性和焦点陷阱,关闭后把焦点恢复到触发按钮;
- 表单:错误提示和字段关联;
- 键盘交互:Tab、方向键操作列表、菜单、选项卡等。
实践上可以:
- 尽量复用已经处理好 a11y 的底层库(如 Radix UI、Headless UI 等);
- 或者在自己实现这些基础能力时给出清晰的约定和示例。
与第三方 UI 库的关系
很多团队会在现有 UI 库(如 antd、MUI、Chakra 等)之上再包一层自己的组件。这一层常常充当:
- 统一主题与风格的入口;
- 封装与业务相关的默认行为;
- 屏蔽不同版本/不同库之间的差异。
典型方式是:
- 底层选一个稳定的 UI 库;
- 在其之上建立自己的
@company/ui封装层,对外只暴露这一层; - 真正使用时,业务项目全部从
@company/ui引入组件。
这样做可以给未来的技术栈调整留出空间。
小结
- 把组件划分成基础、组合和业务三层,有助于在需求变化时知道该动哪一块。
- 基础组件紧贴设计 Token,组合组件固化常见交互模式,业务组件围绕领域对象设计 API。
- 主题与暗色模式应通过 Token 和顶层主题切换实现,组件内部尽量不关心具体颜色值。
- 组件库如果希望长期稳定可用,a11y 和与第三方 UI 库的边界都需要在设计时提前想清楚。