构建 B 端组件(区块)库的一些思考

2,052 阅读6分钟

最近在公司主要负责构建基于 Fusion 的 B 端 React 组件库。前端与 UED 通过产品调研和问题抽象,总结出符合我们业务场景的最佳交互范式,然后前端根据这些范式沉淀出相应的组件库,旨在提升 B 端产品的交互统一性以及开发者的开发效率。下面记录一下在开发过程中对 B 端组件库的一些思考。

组件的分类以及 B 端组件库的定位

依据 Brad Frost 的理论,一个 Design System 中的组件可以有以下分类:

  • Atoms 原子
    • 最细粒度的组件,Button,Input 等。
  • Molecules 分子
    • 由一组原子组成。例如,标签、Input 和按钮构成一个表单元素。它们是 Design System 的基础。
  • Organisms 器官
    • 也就是系统中的区块,由各个分子组成的更为复杂的组件。
  • Templates 模板
    • 它们是由一组「Organisms」组合而成的最终结构,它约定了页面中的实际布局。
  • Pages 页面

从组件的定制化程度来讲,根据我的理解它又可以分为:

  • 基础组件

    • 可以是 Atoms、Molecules、Organisms,它涵盖了 Design System 中的样式以及约定的交互范式, 不关心业务逻辑与相应的接口数据格式。
  • 业务组件

    • 由基础组件组装而成,涵盖具体的业务逻辑(与接口请求)。

在我们的场景中,Fusion 是 Design System 中的 Atoms 部分,而我们的 B 端组件库的定位更偏向于 Molecules 和 Organisms 的集合,同时它又不包含业务逻辑,所以用业界常用的归类来讲,它的定位更偏向于基础区块的集合。

组件的设计原则

我们可以将每个 React 组件想象成是一个黑盒,这种方式很不错。它有自己的输入、生命周期及输出。如果一个组件写得足够清晰易懂,那么使用者就无需查看其内部实现而能快速上手使用。

组件库的建立是为了让开发者快速产出 B 端页面,那么组件必须具有复用性与易用性;同时,为了保证组件库可持续迭代,组件代码必须具有可维护性

我们通过定义组件的设计原则来确保以上三点。

定义 PropTypes

定义 PropTypes 是构建一个合格 React 组件的基本前提。PropTypes 提供一系列校验器,可用于确保组件所接收的 prop 数据类型是有效的。

import React from 'react';
import PropTypes from 'prop-types';

class Greeting extends React.Component {
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    );
  }
}

Greeting.propTypes = {
  name: PropTypes.string
};

组件的样式可以从外部自定义

虽然我们的组件库强约束了一些交互范式,但是这与可自定义性并不矛盾。例如,组件使用者通常需要设置组间的间距,这时可自定义组件的样式就很重要了。

一个组件可以有内置样式,也可以提供在内置样式基础上进行修改的接口。实现方式为:将外部 className 和 style 作为 props,然后与内部的 className 和 style 合并。

const Avatar = ({ className, style, ...props }) => (
  <img 
    className={`avatar ${className}`}
    style={{ borderRadius: "50%", ...style }}
    {...props} 
  />
);
    
Avatar.propTypes = {
  src: PropTypes.string.isRequired,
  alt: PropTypes.string.isRequired,
  className: PropTypes.string,
  style: PropTypes.object
};

props api 应尽量与 Fusion 或 HTML arrtibutes 保持统一

在定义组件 props 时,尽量重用 Fusion 组件或其他组件的 props 命名,统一形成规范。组件的工作原理会更加明确,也会更容易让人记住。例如:

  • Number 的 props api 应与<input type='number' />类似:
<input
  type="number"
  name="number-picker"
  min="10"
  max="100"
  step="10
>

<NumberPicker
  step={3}
  min={6}
  max={30}
  defaultValue={6}
  onChange={onChange} 
/>
  • 基于 Fusion 封装的对话框,在与 Fusion api 保持一致的前提下增加新的功能:
// Fusion Dialog
<Dialog
  title="Title"
  visible={this.state.visible}
  onOk={this.onClose}
  onCancel={this.onClose}
  onClose={this.onClose}>
  This is a dialog
</Dialog>

// 自定义 Dialog
<PollingDialog
  status={this.state.pollingStatus} // 对话框的状态
  titile={this.state.pollingMessage}
  visible={this.state.visible}
  onOk={this.onClose}
  onCancel={this.onClose}
  onClose={this.onClose}
/>

降低组件内置逻辑复杂度

有些组件为了提高其复用性,增添了许多 props 和附加逻辑。例如,一个 Button 组件可以接受 color、size 和形状的 prop。我们有各种各样的需求,这样一来组件的 render 函数逻辑代码会变的越来越多。这时,我们需要停止无脑地给组件增加 prop,学会去「分解」一个组件,从而更加灵活地组织我们的代码去实现需求。「分解」一个组件一般有两种形式:

  1. 新建不同类型的组件

若需要在原有组件的基础上新增一个 feature,在原有组件的基础上新建一个组件往往比新增一个 prop 更好。这样能最保证基础组件的可维护性。

const Button = props => {
  return <button>{props.text}</button>
}

const SubmitButton = () => {
  return <Button text="Submit" />
}

const LoginButton = () => {
  return <Button text="Login" />
}
  1. 使用组合

props.children 可以将其他组件注入到本组件中,实现组件的组合。这样做能够减少 props 的数量,并让组件变得更加「透明」。

//Bad
<Sidebar
  title="title"
  link="link"
/>

// Good
<Sidebar>
  <Title>title</Title>
  <Link>link</Link>
</Sidebar

UI 区块之外, 它还涵盖了什么

抽象出来的通用交互逻辑

在谈到 React 时,我们常常会提到一个公式:

View = f(State)

f 通过处理 State 来形成 View,我们可以将 f 理解为 UI 的交互逻辑。在使用 React 进行开发时,一个优良的设计应该是将 f 函数与视图的组织相分离的,这种模式的其中一种实现方式就是 React 官方文档中提到的状态提升,将子组件的 State 上移至顶部,由顶部组件统一处理 State。

除了状态提升,我们还可以利用 Higher Order Components、Render Props 和 hooks,实现交互逻辑的抽象与复用,这也是组件库的组成部分。下面是一个 Render Props 的例子,Filter 组件和 Table 组件只需要负责纯视图的渲染,查询和列表分页的逻辑已经封装在 TableQuery 中了:

<TableQuery promiseFn={this.getDataSource}>
{(search, update, error, tableProps) => (
  <React.Fragment>
    <Filter style={{ marginBottom: '20px' }} onSearch={search}>
      <FormItem
        label="Gender"
        required
      >
        <Select style={{ width: '100%' }} name="gender">
          <Option value="male">male</Option>
          <Option value="female">female</Option>
        </Select>
      </FormItem>
    </Filter>
    <Table
      primaryKey="email"
      columns={this.columns}
      update={update}
      {...tableProps}
    />
  </React.Fragment>
)}
</TableQuery>

原创的 Atoms 或 Molecules 组件

在实际的业务开发场景中,仅仅基于 Fusion 封装 UI 区块是远远不够的,例如一些特定的业务场景需要定制化的 UI 组件,这些需求可能无法被 Fusion 现有组件覆盖掉,我们需要自己从零开始开发。


参考资料