最近在公司参与实现一套类似ant design功能的组件库。我在项目中主要负责Menu,Tab,Progress, Input,Radio,Checkbox,Select以及Form组件的开发。与其他样式组件不同,Form组件除了显示表单项状态(loading,成功,失败,警告)等以外,还需要对校验逻辑进行封装,提高表单开发效率。功能定位与ant design依赖的rc-menu一致。但个人认为rc-menu的api过于复杂。

设计思路

表单组件根据职责可以拆分为三部分:校验逻辑执行、表单数据管理、表单样式显示,因此表单组件由三部分组成:

  • validator.js提供与DOM无关的校验逻辑执行对象。职责为接受校验规则对象与表单数据对象,返回执行结果(即错误信息)。支持异步和多项联合校验。

  • ValidationFieldformCreatorformCreator为高阶组件。formCreator为被包装的组件管理表单数据,错误信息数据以及校验方法,并通过props和context与子组件通信。ValidationFieldformCreator配合使用,ValidationField接受props或者context的数据和方法将子组件(input、checkbox等)与formCreator的数据绑定。

  • FormItem(名字待定) 负责表单布局和表单输入组件状态的展示。

通过将表单分为这三部分,我们的组件有了更好的灵活性,如果不打算使用我们提供的校验方案,完全可以单独使用FormItem。

在组件的实现中,利用context的地方较多。之前由于了解不够,一直对react的context有所‘恐惧’,认为context是危险的(确实应该慎重使用context)。但经过一段时间的研究,发现合理的使用context是能够大量减少工作量,使得表单组件可以以更少的api覆盖绝大多数情况。有关context的问题请参考这里这里。在这儿还要说明一下,context最重要的问题是在pureComponent下,如果props或者state不变,那么context不能使组件更新。这个问题对于我来说不是问题,整个组件库全部未使用pureComponent或其他类似逻辑。而且context的使用全部由开发人员掌握,不会要求使用者对context处理(唯一的要求就是validationField与FormCreator返回的组件之间不要有purecomponent组件包裹)。此外使用context的组件均提供了对应的prop作为替代选项。

这里还需要说明组件库不使用pureComponent的原因,使用pureComponent优化性能在实际项目中很有必要,但是作为基础组件库,使用pureComponent会带来浅比较的成本,所以这里有个tradeoff。如果使用者真的需要pureComponent的话,完全可以在我们提供的组件上在包装一层。‘过早优化是万恶之源’这句话我感觉适用于这里的情况。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React, { PureComponent } from 'react';

import {
formCreator,
FormItem,
ValidationField,
Input,
Button,
} from '../src';

const rules = {
name: {
validator(name) {
if (name === 'loading') {
return true;
}
return false;
},
message: name => `"${name}" is not my name!`,
},
};

class ValidateForm extends PureComponent {
render() {
const { onAllValidate } = this.props;
return (
<div>
<ValidationField label="Name" name="name" validateTrigger="onBlur">
<Input placeholder="值必须为loading" />
</ValidationField>
<ValidationField name="date" label="Birth" helperText="值必须为1995">
<Input />
</ValidationField>
<FormItem>
<Button onClick={onAllValidate}>check all</Button>
</FormItem>
</div>
);
}
}

const Demo = formCreator()(ValidateForm);