构建 B 端组件库的一些思考
date
May 31, 2020
slug
b-system-materials-thoughts
status
Published
tags
Design System
summary
type
Post
最近在公司主要负责构建基于 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 的集合,同时它又不包含业务逻辑,所以用业界常用的归类来讲,它的定位更偏向于基础区块的集合;而按照阿里集团的定义来讲,它的定位就是物料库(Materials)。
组件的设计原则
我们可以将每个 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,学会去「分解」一个组件,从而更加灵活地组织我们的代码去实现需求。「分解」一个组件一般有两种形式:
- 新建不同类型的组件
若需要在原有组件的基础上新增一个 feature,在原有组件的基础上新建一个组件往往比新增一个 prop 更好。这样能最保证基础组件的可维护性。
const Button = props => {
return <button>{props.text}</button>
}
const SubmitButton = () => {
return <Button text="Submit" />
}
const LoginButton = () => {
return <Button text="Login" />
}
- 使用组合
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 现有组件覆盖掉,我们需要自己从零开始开发。
题外话:业务强相关的逻辑与交互怎么定位
最近团队内又开始对业务强相关的逻辑怎么进行复用进行了讨论,讨论到后来很容易回到一个词:业务组件。
而我的思考是,是否需要存在「业务组件」这个概念。
我们所讨论的业务组件,本质上是想将业务强相关的逻辑与交互进行复用与抽象,从灵活性和可推广性来讲业务逻辑应该与 UI 分离,而不是以组件的形式呈现。甚至可以不需要有「业务组件库」这个东西,更应该去沉淀一套「业务逻辑库」or 「业务模型库」。