如何让组件库兼顾灵活性和一致性
做B端前端最痛苦的事:组件库管太死,业务方抱怨"这破东西根本满足不了需求";管不住,设计稿和代码各走各的,一致性全面崩塌。
这不是二选一的问题,是你架构没搭好。
问题本质:为什么这是个伪命题
先搞清楚两件事。
SaaS多租户需要灵活性。租户A要蓝色主题,租户B要绿色主题,你不能改代码,只能靠配置。
企业级系统需要一致性。100个页面必须有统一的交互逻辑,新人接手不会懵。
这两个需求同时存在于B端产品里。你没办法通过"规范约束"解决结构性问题。
一致性失控的信号
你的组件库已经出问题了,如果你看到这些:
- Button组件有12种颜色变体
- Modal有4种不同的关闭方式
- Select的placeholder有"请选择"、"请输入"、"请选择内容"三种文案
每个变体都有人"合理地"创建出来。然后维护成本爆炸。
灵活性失控的信号
反过来的问题:
- 3个业务方fork了你的组件库
- 主库半年没人更新,因为没人敢动
- 每次升级都是大型Battle现场
当业务方开始fork,说明你的组件库跟他们没关系了。
三层架构:灵活性与一致性的解法
我的方案是分层设计。
| 层级 | 职责 | 业务方能改吗 |
|---|---|---|
| 基础层 | 纯UI原子,零业务逻辑 | 不能 |
| 组合层 | 基于基础层组合业务逻辑 | 可以,但有约束 |
| 配置层 | 运行时配置,零代码定制 | 完全开放 |
基础层:锁定,不妥协
Button、Input、Icon、Badge这些原子组件是基础设施,不业务化。
// ✅ 对的:基础组件只有属性,没有业务假设
<Button type="primary" size="large" />
// ❌ 错的:业务变体混入基础组件
<Button type="primary" intent="danger" meaning="delete" />
intent、meaning 这些业务概念不应该出现在 Button 的props里。
组合层:给空间,但要守规矩
组合层基于基础层组合出业务组件。这里可以定制,但要遵守协议。
// 业务组件:SearchFilter
// 业务方可以改,但不能破坏接口契约
interface SearchFilterProps {
// 这些是契约,业务方必须实现
onSearch: (values: Record<string, any>) => void;
onReset: () => void;
// 扩展点:业务方可以注入自定义字段
extraFilters?: React.ReactNode;
// 锁定:内部状态业务方碰不到
// privateState?: any; // 不暴露
}
组合层的关键是明确什么能改,什么不能改。
配置层:完全开放,零门槛
主题配置、Feature Flags、业务参数都放在配置层。
// theme-config.json
{
"primaryColor": "#1890ff",
"enabledModules": ["export", "batchDelete"],
"dateFormat": "YYYY-MM-DD"
}
业务方改配置就行,不需要改代码。
4个关键设计模式
1. Design Token:用变量锁定视觉一致性
/* tokens.css */
:root {
--color-primary: #1890ff;
--color-danger: #ff4d4f;
--space-md: 16px;
--radius-md: 6px;
}
// 组件里用token,不直接写死颜色
const Button = ({ type }) => (
<button
className={type}
style={{ backgroundColor: 'var(--color-primary)' }}
>
{children}
</button>
);
改主题?换一个token文件就行。组件代码不用动。
2. Compound Components:让内部组合更灵活
// 反模式:所有props堆在一起
<Select
placeholder="请选择"
options={[]}
onChange={handleChange}
showSearch
allowClear
multiple
tags
// ...20个props
/>
// 正模式:组合模式
<Select placeholder="请选择">
<Select.Option value="1">选项1</Select.Option>
<Select.Option value="2">选项2</Select.Option>
<Select.Search />
<Select.Creatable /> {/* 按需引入 */}
</Select>
Compound Components 让使用者从"记props"变成"组合能力"。灵活性来自组合,一致性来自约束。
3. Props Schema 收敛:统一接口
不一致的接口是维护噩梦:
// 三种写法,折磨人
onChange: (value) => {} // Ant Design
handleSelect: (value) => {} // 某业务组件
onItemClick: (item, index) => {} // 又一个组件
我的规则:
- 选中/变化事件:统一用
onChange: (value, item?) => void - 确认事件:统一用
onConfirm: () => void - 取消事件:统一用
onCancel: () => void
事件命名收敛后,业务方根本不需要查文档。
4. Slot 机制:扩展点设计
interface CardProps {
title: string;
extra?: React.ReactNode; // 标题栏右侧扩展
footer?: React.ReactNode; // 底部扩展
children?: React.ReactNode;
}
// 使用方
<Card title="用户列表" extra={<Button>导出</Button>}>
<UserTable />
</Card>
扩展点让组件在锁定结构的同时,支持任意位置的内容定制。
治理规范:光靠设计不够,还得管
组件准入门槛
新组件入库必须满足:
- 至少3个业务场景在用
- 有完整的设计稿
- 通过一致性评审
防止"拍脑袋"组件入库,半年后没人维护。
变更评估清单
每次改动回答3个问题:
- 影响范围:这次改动破坏几个现有组件?
- 迁移成本:业务方需要改多少代码?
- 为什么不能通过配置解决?
如果能通过配置解决,就不要改组件。
自动化检查
// package.json scripts
{
"lint:components": "eslint src/components --rule 'no-unstable-nested-components: error'"
}
一致性检查进CI。不合规的代码根本merge不进去。
最后
好的组件库不是告诉业务方"你按我说的做",而是"你想做的,我支持你做"。
一致性不是约束出来的,是架构设计出来的。
相关推荐