如何设计一个优秀的组件

1,068 阅读6分钟

阅读更多系列文章请访问我的GitHub 博客,本文示例代码请访问这里

前言

在开发过程中,我们经常会遇到现有组件库无法满足需求,需要自己设计和实现组件的情况。那么,如何才能设计一个既满足产品需求,又易于开发人员使用的组件呢?本文就以一个级联组件的设计为例,探讨一下如何设计一个优质组件。

开始前准备

  1. 如果你想查看本文的 Demo 演示,请点击这里
  2. 如果想查看源代码,请点击这里
  3. 本文的源代码都由 TypeScript 编写,如果你对 TypeScript 不了解,可以查看TypeScript 官网
  4. 本文 Demo 中的级联组件,是基于Ant Design开发的,如果你对它不了解,可以先查看组件总览

接下来我将详细叙述一下组件的设计与实现过程。

组件设计

需求分析

开发这个组件的原因是来自工作中遇到的一个需求。

产品提出的业务需求是实现一个地区的级联选择,并且应设计师要求,不能使用 Ant Design 的Cascader 级联选择,因此需要自己实现一个级联选择。

通常需要从 4 个角度对需求进行分析:

  1. 业务需求角度
  2. UI 设计角度
  3. 开发人员角度
  4. 与其他组件配合角度

业务需求角度

在拿到一个需求之后,我们不应当直接考虑如何实现,而要先思考一下,这个需求是不是真正的需求。或者说,这个需求背后,是否还有更加基础和核心的需求。

现在,我们拿到了一个地区级联选择的需求,那么我们应该仅仅实现地区级联选择吗?我认为不是的,今天我们拿到的是地区选择的需求,明天就可能变成职业选择。

因此,我们实际需要实现的,应该是一个支持N 级的级联选择框。

UI 设计角度

既然已经确定了要实现 N 级级联选择框,就需要考虑其在各个屏幕尺寸的兼容情况,于是我在组件中引入了 Grid 栅格 。并默认设置了 3 级级联的样式,同时为使用者提供了相应的配置选项,如下:

```
<Cascade
  rowProps={{
    gutter: 10,
  }}
  colProps={{
    xs: 24,
    sm: 24,
    md: 8,
    lg: 8,
    xl: 8,
  }}
/>
```

开发人员角度

我们开发的组件,不止会被我们的团队成员使用,甚至会开源给其他开发者使用。那么,提供给开发者更好的体验就尤为重要。

为了节省开发人员的学习成本,我们可以按照以下思路考虑:

  1. 在满足基本需求的前提下,提供尽量少的 API。
  2. 为 API 选项提供尽量少的配置项。
  3. 为代码,特别是 API 提供尽量好的文档或注释。

定义的 Cascade 组件 Props 示例如下:

```
interface Props<T> {
  cascadeKeys?: CascadeKeys; // 自定义 dataSource 中 value label children 的字段
  value?: T[]; // 指定当前选中的条目
  onChange?: (value: T[], level: number) => void; // 选中选项时,调用此函数
  rowProps?: RowProps; // 行排列方式,可参考https://ant.design/components/grid-cn/
  colProps?: ColProps; // 列排列方式
  loading?: boolean[]; // 选择框loading装填
  dataSource?: T[] | CascadeData<T> | T[][]; // 可选项数据源
}
```

可以看到, Props 中只有 dataSource 是必须的。也就是说,如果你对配置项并不了解,组件也只需要最简单的配置,就可以正常工作了,例如:

<Cascade
  dataSource={pcaCascadeData}
/>
  1. 在使用 TypeScript 时,还需要特别考虑类型匹配的问题。例如可以在使用组件时,传入一个类型,并且在 onChange 事件中如果使用了其他类型, TypeScript 检查就会提示错误,如下面例子中的 PCAItem

    <Cascade<PCAItem>
      dataSource={pcaCascadeData}
      cascadeKeys={pcaCascadeKeys}
      onChange={async (value: PCAItem[], level: number) => {
        setPCAData(value);
        setPCAIndex(level);
      }}
    />
    
  2. 对于级联组件,我们还需要考虑 dataSource 的数据来源可能有两种。

    • 组件初始化时,就传入了所有的级联数据,例如 省/地/县/乡层级数据 。对应 Demo 中的“同步级联数据”,以及 PropsdataSource 类型定义的 CascadeData<T> | T[][]

      • 在某些场景下,虽然没有级联选择框存在,但也需要处理树形数据,包括数据的查询和校验等功能,因此将该方法封装到 CascadeData 类中。
      • 考虑到树的数据量可能非常庞大,如果在每次选择时都在树中搜索效率较低。因此设计成在组件创建时,直接遍历树中的所有节点,然后将每个层级所有节点的数据都存储在相应的 Map 中,之后就能很方便地查询数据。
      • 虽然在组件初始化时遍历所有节点比较耗时,但考虑到用户从进入页面到操作组件有一定时间差,因此我认为这个问题可以忽略。
      • 基于以上考虑,你在 PropsdataSource 类型中看到的 CascadeData<T> ,就表示直接传入了一个 new CascadeData(treeData) 。而T[][]就表示直接传入树形数据,由组件内部进行 new CascadeData(treeData)
    • 组件初始化时,只传入了第一级的选项,之后每级的选项通过前一级所选择的参数,从服务端获取。对应 Demo 中的“异步级联数据”,对应 dataSource 类型中的 T[]

      • 组件初始化时,只传入第一级的数据,其他级别传入空数组,如:[[{"code":110000,"name":"北京市"}],[],[]],组件会渲染出 3 层级联选项。
      • 当进行选择时,需要使用者通过 onChange 事件自行更新下一级的数据。也就是说,组件完全放弃了对数据的控制。

与其他组件配合角度

由于该组件需要与 Ant Design 其他组件配合使用,如前面讨论过的 UI 部分,该组件就结合了 Grid 栅格 组件。既保证了该组件在各屏幕宽度下正常显示,又保证了与其他组件的显示一致。

除此之外,还需要考虑与 Form 表单 组件的配合,特别是兼容表单校验功能。

结语

本文通过一个级联组件的设计案例,探讨了如何从四个角度分析,进而设计一个优秀的组件。这 4 个角度分别是:

  1. 业务需求角度
  2. UI 设计角度
  3. 开发人员角度
  4. 与其他组件配合角度

我认为很多时候组件的设计并没有最优解,总是需要根据需求在各种方案中取舍。但只要按照本文提到的 4 个角度进行分析,就能设计出优秀的组件。