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 有四十多个。使用者只想展示一个简单的列表,却要传一堆用不上的配置项。每次用这个组件,都得翻半天文档。

我曾经踩过的坑:设计了一个「全能表单组件」,一个 props 控制校验规则,另一个控制布局,还有一个控制提交方式。结果团队成员每次用都要问我:「这个参数到底要不要传?」后来我重构了,拆成了 FormInputFormSelectFormSubmit 三个独立组件,每个接口都精简到 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 可复用性设计

可复用性,说白了就是「一次编写,多处使用」。但这里有个误区:很多人为了复用,把组件设计得特别通用,结果每个场景都用得不顺手。

我个人习惯是:先写具体场景的组件,等发现重复代码时再抽象。别一开始就想着「万能」,那往往是过度设计的开始。

我的经验:可复用组件通常具备三个特征——可配置、可扩展、可预测。可配置指通过 props 控制行为;可扩展指支持插槽或自定义渲染;可预测指输入输出稳定,不会出现意外行为。

举个例子。我之前写了一个 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>
  );
}
注意:可复用不等于「一次设计永久使用」。随着业务变化,组件需要迭代。我建议每个组件都保留重构的余地,别把代码写死。

小结

这四条原则,其实是一体的。单一职责让组件专注,接口隔离让组件易用,高内聚低耦合让组件稳定,可复用性让组件有价值。

你想想看,如果每个组件都遵循这些原则,你的项目会变成什么样?组件像乐高积木一样,可以自由组合、替换、升级。改一个组件不会影响其他组件,新需求来了只需要组合现有组件。

嗯,这就是我理想中的组件化开发。下一章,我会聊聊组件通信的几种模式,到时候见。

公众号:蓝海资料掘金营,微信 deep3321