2、组件设计原则:单一职责原则、接口隔离原则、高内聚低耦合、可复用性设计
聊到组件设计,我脑子里第一个蹦出来的词就是「原则」。
说实话,我刚入行那会儿,写组件全凭感觉。一个按钮组件里塞了十几种样式配置,一个弹窗组件能控制整个页面的状态。结果呢?改一个地方,崩一片。后来我才明白,组件设计不是堆功能,而是做取舍。
下面这四条原则,是我这些年踩坑踩出来的。你想想看,如果能吃透它们,你的组件质量至少提升一个档次。
2.1 单一职责原则
什么叫单一职责?说白了,一个组件只做一件事。
我见过太多「万能组件」了。一个 Input 组件,既能输入文本,又能做下拉选择,还能当日期选择器用。代码里全是 if-else 分支,维护起来简直噩梦。
- 一个组件只负责一个功能领域
- 变化的原因只有一个
- 如果组件需要多个理由去修改,就该拆分了
举个例子。我之前在项目中遇到一个需求:用户头像展示。一开始我写了个 UserAvatar 组件,它既负责加载头像图片,又负责处理上传逻辑,还管着用户状态的显示。
// ❌ 违反单一职责
class UserAvatar {
loadAvatar() { /* 加载图片 */ }
uploadAvatar() { /* 上传图片 */ }
showStatus() { /* 显示在线状态 */ }
editProfile() { /* 编辑资料 */ }
}
后来我拆成了三个组件:AvatarImage 只负责图片展示,AvatarUploader 只负责上传,UserStatusBadge 只负责状态。每个组件都变得简单、可测试。
// ✅ 符合单一职责
class AvatarImage {
render() { /* 只展示图片 */ }
}
class AvatarUploader {
upload() { /* 只处理上传 */ }
}
class UserStatusBadge {
render() { /* 只显示状态 */ }
}
2.2 接口隔离原则
接口隔离原则,听起来高大上,其实就一句话:不要强迫使用者依赖他们不需要的东西。
我见过最典型的反面教材:一个 Table 组件,props 有四十多个。使用者只想展示一个简单的列表,却要传一堆用不上的配置项。每次用这个组件,都得翻半天文档。
FormInput、FormSelect、FormSubmit 三个独立组件,每个接口都精简到 5 个 props 以内。
接口隔离的核心做法:
- 最小接口原则:只暴露必要的 props 和方法
- 按需组合:用组合代替继承,让使用者自由搭配
- 默认值兜底:能自动处理的就别让使用者操心
// ❌ 接口过于臃肿
<Table
data={data}
columns={columns}
pagination={true}
pageSize={10}
showHeader={true}
striped={false}
bordered={true}
hoverable={true}
onRowClick={handleClick}
onSort={handleSort}
onFilter={handleFilter}
// ... 还有 30 个 props
/>
// ✅ 接口精简,按需组合
<Table data={data} columns={columns}>
<Table.Pagination pageSize={10} />
<Table.Sortable onSort={handleSort} />
</Table>
2.3 高内聚低耦合
这个原则我特别喜欢。它讲的是:组件内部要紧密协作,组件之间要尽量独立。
高内聚,就是组件内部的东西应该高度相关。比如一个 DatePicker 组件,它的日期计算、UI 渲染、事件处理都应该放在一起。别把日期格式化逻辑扔到外面去。
低耦合,就是组件之间不要有太多依赖。我见过有人把两个完全不相关的组件通过全局状态管理绑在一起,改一个就得改另一个,牵一发而动全身。
- 高内聚:组件内部修改时,不影响外部
- 低耦合:外部修改时,不需要改动组件内部
我在项目中常用的做法:
- Props 驱动:组件只通过 props 接收数据,不直接引用外部状态
- 事件回调:组件通过回调函数通知父组件,不直接操作外部逻辑
- 避免全局依赖:能用 props 传的,就别用全局变量
// ❌ 高耦合:组件直接引用外部状态
function UserCard() {
const user = window.__GLOBAL_USER__; // 依赖全局变量
const theme = useContext(ThemeContext); // 依赖上下文
return <div>{user.name}</div>;
}
// ✅ 低耦合:通过 props 传入
function UserCard({ user, theme }) {
return <div className={theme.card}>{user.name}</div>;
}
2.4 可复用性设计
可复用性,说白了就是「一次编写,多处使用」。但这里有个误区:很多人为了复用,把组件设计得特别通用,结果每个场景都用得不顺手。
我个人习惯是:先写具体场景的组件,等发现重复代码时再抽象。别一开始就想着「万能」,那往往是过度设计的开始。
举个例子。我之前写了一个 Modal 组件,一开始只支持文本内容。后来发现很多场景需要自定义内容,于是加了 children 插槽。再后来需要不同尺寸,又加了 size 属性。每次都是按需添加,而不是一开始就堆满功能。
// 可复用的 Modal 组件设计
function Modal({
visible,
title,
children, // 可扩展的内容区域
size = 'medium', // 可配置的尺寸
onClose, // 可预测的回调
footer // 可自定义的底部
}) {
return (
<div className={`modal modal-${size}`}>
<header>{title}</header>
<main>{children}</main>
<footer>{footer || <button onClick={onClose}>关闭</button>}</footer>
</div>
);
}
小结
这四条原则,其实是一体的。单一职责让组件专注,接口隔离让组件易用,高内聚低耦合让组件稳定,可复用性让组件有价值。
你想想看,如果每个组件都遵循这些原则,你的项目会变成什么样?组件像乐高积木一样,可以自由组合、替换、升级。改一个组件不会影响其他组件,新需求来了只需要组合现有组件。
嗯,这就是我理想中的组件化开发。下一章,我会聊聊组件通信的几种模式,到时候见。