前言

在选择使用 react 技术栈的时,往往都会选择一套成熟好用而且自己熟悉的 UI 框架,而我使用比较多的ant Design,这样可以减少开发成本与风险,因为自己开发的组件总会有考虑不周的 BUG,而 antd 里有很多优秀的组件,但我觉得在表单方面,antd 的这个组件做的很优秀,也很灵活,它的核心也在校验,这篇主要是 Form 校验。

表单作用

真正需要表单的时候,一般都因为以下功能:

  1. 收集各表单项的数据。
  2. 按照要求,对表单项数据进行校验,并显示校验结果。
  3. 最总获取所有的表单数据,并表单中所有数据进行校验。

而这些功能 antd 都可以直接按官网案例使用,简单快速,但 antd 实现这些功能也是很简单的,主要是因为它把数据收集与校验都交给了 rc-form 去实现,剩下的 UI 展示才是 antd 实现的。

antd 的 Form 实现

Form.tsx

在 form 的 Form.tsx 只是简单的实现了 Form 的 UI 与 rc-form 的 create 表单,使用 createReactContext 传递了通用布局属性。

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
// 此处为Form.create的实现,主要调用了createBaseForm
// fieldMetaProp规定了重造组件里存储的表单规则,如校验等,fieldDataProp规定了表单的数据,所以获取到react组件,就可以知道表单属性设置与数据

static create = function create<TOwnProps extends FormComponentProps>(
options: FormCreateOption<TOwnProps> = {},
): FormWrappedProps<TOwnProps> {
return createDOMForm({
fieldNameProp: 'id',
...options,
fieldMetaProp: FIELD_META_PROP,
fieldDataProp: FIELD_DATA_PROP,
});
};

// 创建表单与createReactContext传递UI布局属性

render() {
const { wrapperCol, labelAlign, labelCol, layout, colon } = this.props;
return (
<FormContext.Provider
value={{ wrapperCol, labelAlign, labelCol, vertical: layout === 'vertical', colon }}
>
<ConfigConsumer>{this.renderForm}</ConfigConsumer>
</FormContext.Provider>
);
}

FormItem.tsx

实现了需要校验组件的布局与校验信息显示

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// 通过这两个方法获取到组件上的所有表单信息,如值、校验信息,获取的方式就是在组件的props的FIELD_META_PROP与FIELD_DATA_PROP上。
getMeta() {
return this.getChildProp(FIELD_META_PROP);
}

getField() {
return this.getChildProp(FIELD_DATA_PROP);
}

// 通过getValidateStatus获取校验的状态

getValidateStatus() {
const onlyControl = this.getOnlyControl();
if (!onlyControl) {
return '';
}
const field = this.getField();
if (field.validating) {
return 'validating';
}
if (field.errors) {
return 'error';
}
const fieldValue = 'value' in field ? field.value : this.getMeta().initialValue;
if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
return 'success';
}
return '';
}

// renderValidateWrapper根据校验状态,选择html结构与css类名,显示校验信息

renderValidateWrapper(
prefixCls: string,
c1: React.ReactNode,
c2: React.ReactNode,
c3: React.ReactNode,
) {
const { props } = this;
const onlyControl = this.getOnlyControl;
const validateStatus =
props.validateStatus === undefined && onlyControl
? this.getValidateStatus()
: props.validateStatus;

let classes = `${prefixCls}-item-control`;
if (validateStatus) {
classes = classNames(`${prefixCls}-item-control`, {
'has-feedback': props.hasFeedback || validateStatus === 'validating',
'has-success': validateStatus === 'success',
'has-warning': validateStatus === 'warning',
'has-error': validateStatus === 'error',
'is-validating': validateStatus === 'validating',
});
}

let iconType = '';
switch (validateStatus) {
case 'success':
iconType = 'check-circle';
break;
case 'warning':
iconType = 'exclamation-circle';
break;
case 'error':
iconType = 'close-circle';
break;
case 'validating':
iconType = 'loading';
break;
default:
iconType = '';
break;
}

const icon =
props.hasFeedback && iconType ? (
<span className={`${prefixCls}-item-children-icon`}>
<Icon type={iconType} theme={iconType === 'loading' ? 'outlined' : 'filled'} />
</span>
) : null;

return (
<div className={classes}>
<span className={`${prefixCls}-item-children`}>
{c1}
{icon}
</span>
{c2}
{c3}
</div>
);
}

所以使用 antd Form 需要把组件放到 Form 与 FormItem 里,否则会出现 UI 问题。

rc-form

rc-form 的实现是使用了高阶组件,这里不说高阶组件,然后利用闭包的收集数据,类似与全局变量,创建一个对象,根据 ID 收集放到对应的 ID 里,简单就是这样。
源码主要看 createBaseForm 与 createFieldsStore,createBaseForm 主要是实现功能,如校验,获取数据等等,而 createFieldsStore 主要实现数据收集与处理,然后让 createBaseForm 使用。

createBaseForm 数据的更新是使用了 forceUpdate,这是 react 强制更新组件的方法,可以在没有触发 setState 时触发 render 来实现数据更新,它的实现与 setState 差不多。

mapPropsToFields 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
// 初始化
getInitialState() {
const fields = mapPropsToFields && mapPropsToFields(this.props);
this.fieldsStore = createFieldsStore(fields || {});
...
}
// 父组件state变化
componentWillReceiveProps(nextProps) {
if (mapPropsToFields) {
this.fieldsStore.updateFields(mapPropsToFields(nextProps));
}
}

由于在创建组件时都用 Form 包含,所以组件之间需要经过 Form,那么就可以利用这个实现数据映射关系。

所有的校验都会统一走 validateFieldsInternal 方法,而一般的回填与映射是不会触发校验的,只有符合事件的或手动调用校验才会触发校验,而校验使用的async-validator这个包。

注意事项:

  1. 由于 rc-form 的 ID 允许嵌套,所以 ID 避免实现”.”与[,如 abc.mm,abc[0]
  2. rc-form 校验的 type 为 Array 时,避免 ID 带数字,如 abc111m1

具体实现可以查看资料