后台开发中,表单的需求非常的频繁。面对大量的表单需求,antd
提供的Form
组件提供的功能足够强大,学习成本也非常的低。
那这个Form
组件内部帮我们做了什么事情呢?
Form.useForm()
如何生成form
实例?
onFinish
是怎么被执行的?
Form.Item
的name
是怎么被利用起来的,rules
配置的验证规则又是怎么触发的?
请带着以上这些问题,跟我一起对Form
组件的核心依赖rc-field-form
进行了源码学习与仿造。
我将带着你实现以下功能:
- 表单存取值
- 表单初始值
- 表单验证
- 表单提交
- 重置表单
- 值改变监听
食用方式:跟着文章动手实现属于你自己的表单组件,同时比对rc-field-form源码加深理解。
文章考虑到可读性,没有在代码块中加入类型,可以在项目源码中获取。
rc-field-form使用案例
import Form, { Field } from 'rc-field-form';
<Form
onFinish={values => {
console.log('Finish:', values);
}}
>
<Field name="username">
<Input placeholder="Username" />
</Field>
<Field name="password">
<Input placeholder="Password" />
</Field>
<button>Submit</button>
</Form>;
复制代码
项目初始化
做为一款组件,选择使用dumi
快速生成项目。
mkdir base-field-form && cd base-field-form
yarn create @umijs/dumi-lib
如果你不了解dumi
可以点击了解。
生成项目后目录结构如下:
docs是dumi的文档目录,src是开发目录。
组件搭建
首先我们来看一下Form
内部流程图:
大家第一次看到这个图一定是一头雾水的,没有关系,我们来逐个讲一下这些模块的作用以及调用关系。
首先搭出组件的结构,在src
目录下新建FieldForm
目录:
FieldForm
- index.tsx
- Form.tsx
- Field.tsx
- useForm.js
复制代码
Form.tsx
内容如下:
const Form: React.FC<any> = props => {
const { children } = props;
return <form>{children}</form>;
};
export default Form;
复制代码
再来看Field.tsx
:
class Field extends Component<any> {
getControled = () => {
const { name } = this.props;
return {
value: '', // TODO
onChange: (e: any) => {
// TODO
},
};
};
render() {
const { children } = this.props;
const returnChildNode = React.cloneElement(
children as React.ReactElement,
this.getControled(),
);
return returnChildNode;
}
}
export default Field;
复制代码
接着就是核心的useForm
:
export default function useForm() {
const formRef = useRef();
return [formRef.current];
}
复制代码
在使用antd Form
开发的时候,利用useForm
生成的Form
实例做验证、提交、重置表单等功能,这些功能的基础是它先要有一个内部的仓库来集中管理数据。
数据仓库
class FormStore {
// 用来保存表单数据
private store: Store = {};
getFieldValue = (name: string) => this.store[name];
getFieldsValue = () => this.store;
setFieldsValue = (newStore: any) => {
this.store = {
...this.store,
...newStore,
};
};
getForm = () => {
return {
getFieldValue: this.getFieldValue,
getFieldsValue: this.getFieldsValue,
setFieldsValue: this.setFieldsValue,
};
};
}
复制代码
我们创建FormStore
来作为数据仓库,并提供相应的函数来操作、获取数据。下一步让useForm
提供FormStore
,使其能拥有对应的能力:
const useForm = (form?: FormInstance) => {
const formRef = useRef<FormInstance>();
if (!formRef.current) {
if (form) {
formRef.current = form;
} else {
const formStore = new FormStore();
formRef.current = formStore.getForm();
}
}
return [formRef.current];
};
export default useForm;
复制代码
存储数据的地方有了,Field.tsx
同时对值的改变、获取进行了拦截。那下一步需要解决的问题就是怎么让Field.tsx
中值的改变、获取与useForm
绑定起来了。
rc-field-form
上交的答案是React.createContext。
如果你对
React.createContext
不了解,建议先学习掌握该知识点。
创建文件FieldContext.tsx
:
import * as React from 'react';
const warningFunc: any = () => {
console.warn('Can not find FormContext. Please make sure you wrap Field under Form.');
};
const Context = React.createContext<FormInstance>({
getFieldValue: warningFunc,
getFieldsValue: warningFunc,
setFieldsValue: warningFunc,
});
export default Context;
复制代码
OK,开始改造Form
,引入FieldContext
、useForm
。这里完成的是把useForm
提供的实例作为FieldContext
的值进行绑定:
import React from 'react';
import FieldContext from './FieldContext';
import useForm from './useForm';
const Form: React.FC<any> = props => {
const { children, form, ...restProps } = props;
const [formInstance] = useForm(form);
return (
<form {...restProps}>
<FieldContext.Provider value={formInstance}>
{children}
</FieldContext.Provider>
</form>
);
};
export default Form;
复制代码
Form.tsx
中提供了 Context
,在Field.tsx
中对它进行消费了:
import React, { Component } from 'react';
import FieldContext from './FieldContext';
class Field extends Component<any> {
public static contextType = FieldContext;
getControled = () => {
const { name } = this.props;
const { getFieldValue, setFieldsValue } = this.context;
return {
value: getFieldValue(name),
onChange: (...args: EventArgs) => {
const event = args[0];
if (event && event.target && name) {
setFieldsValue({
[name]: (event.target as HTMLInputElement).value,
});
}
},
};
};
render() {
const { children } = this.props;
const returnChildNode = React.cloneElement(
children as React.ReactElement,
this.getControled(),
);
return returnChildNode;
}
}
export default Field;
复制代码
现在已经完成了Form
提供Context
,Field
中消费了Context
以完成值的获取和更新。
来查看一下目前的成果:
上面动图中打印的值是在setFieldsValue
输出的。很明显存在一个问题,store
成功赋值,组件却没有更新。
组件更新
值更新了,组件没有更新,是因为setFieldsValue
对store
的改变,没有去触发组件的重新渲染,所以我们需要在setFieldsValue
改变store
的同时,重新渲染与之对应的组件。
明白了其中的缘由,再来看rc-field-form
的解决步骤:
useForm
提供「内部」的注册方法, 注册Field
组件到「内部变量」fieldEntities
中。
// useForm.tsx新增
// 用于存储Field
private fieldEntities: FieldEntity[] = [];
private registerField = (entity: FieldEntity) => {
this.fieldEntities.push(entity);
// un-register field callback
return () => {
this.fieldEntities = this.fieldEntities.filter((item) => item !== entity);
};
};
复制代码
Field
初始化的时候,把自己注册功能到fieldEntities
中去。同时Field
提供组件刷新的功能函数(onStoreChange
)
// Field.tsx
import React, { Component } from 'react';
import FieldContext from './FieldContext';
class Field extends Component<any> {
public static contextType = FieldContext;
private cancelRegister: any;
componentDidMount() {
const { registerField } = this.context;
this.cancelRegister = registerField(this);
}
componentWillUnmount() {
this.cancelRegister && this.cancelRegister();
}
public onStoreChange = () => {
this.forceUpdate();
};
getControled = () => {
const { name } = this.props;
const { getFieldValue, setFieldsValue } = this.context;
return {
value: getFieldValue(name),
onChange: (...args: EventArgs) => {
const event = args[0];
if (event && event.target && name) {
setFieldsValue({
[name]: (event.target as HTMLInputElement).value,
});
}
},
};
};
render() {
const { children } = this.props;
const returnChildNode = React.cloneElement(
children as React.ReactElement,
this.getControled(),
);
return returnChildNode;
}
}
export default Field;
复制代码
- 在
setFieldsValue
的时候,调用对应Field
的onStoreChange
完成组件的更新。
// 更新useForm.tsx中setFieldsValue方法
setFieldsValue = (newStore: any) => {
this.store = {
...this.store,
...newStore,
};
this.getFieldEntities(true).forEach(({ props, onStoreChange }) => {
const name = props.name as string;
Object.keys(newStore).forEach((key) => {
if (name === key) {
onStoreChange();
}
});
});
};
复制代码
看一下更新后的效果:
表单验证与表单提交
有了数据仓库,实现valdate
和submit
方法也水到渠成了。
表单验证
在useForm
中增加validateFields
:
private validateFields = () => {
const promiseList: Promise<{
name: string;
errors: string[];
}>[] = [];
this.getFieldEntities(true).forEach(entity => {
const promise = entity.validateRules();
const { name } = entity.props;
promiseList.push(
promise
.then(() => ({ name, errors: [] }))
.catch((errors: any) =>
Promise.reject({
name,
errors,
}),
),
);
});
let hasError = false;
let count = promiseList.length;
const results: FieldError[] = [];
const summaryPromise = new Promise((resolve, reject) => {
promiseList.forEach((promise, index) => {
promise
.catch(e => {
hasError = true;
return e;
})
.then(result => {
count -= 1;
results[index] = result;
if (count > 0) {
return;
}
if (hasError) {
reject(results);
}
resolve(this.getFieldsValue());
});
});
});
return summaryPromise as Promise<Store>;
};
复制代码
梳理一下validateFields
的流程:
- 声明异步列表
promiseList
用来存储需要验证的规则。 - 遍历
this.fieldEntities
以获取每一个Field
实例的验证方法,并将其放入promiseList
。 - 初始化验证结果标记
hasError
、验证结果results
、需要验证的规则总数count
。 - 利用
summaryPromise
返回Promise
。summaryPromise
内部遍历promiseList
,遇到验证失败时把验证结果设置为错误,并且自减count
,直到count
为0时返回验证结果。
上面看到了Field
的validateRules
方法,在Field.tsx
中创建validateRules
方法:
public validateRules = () => {
const { rules, name } = this.props;
if (!name || !rules || !rules.length) return [];
const cloneRule = [...rules];
const { getFieldValue } = this.context;
const value = getFieldValue(name);
const promise = validateRules(name, value, cloneRule);
promise
.catch(e => e)
.then(() => {
if (this.validatePromise === promise) {
this.validatePromise = null;
this.onStoreChange();
}
});
return promise;
};
复制代码
最终验证功能交给了validateRules
方法,validateRules
其实是用async-validator做了验证,有兴趣的小伙伴可以看一下项目源码下的utils/validateUtil.ts
。
表单提交
在useForm.tsx
中新增:
private submit = async () => {
this.validateFields()
.then(values => {
const { onFinish } = this.callbacks;
if (onFinish) {
try {
onFinish(values);
} catch (err) {
console.error(err);
}
}
})
.catch(e => {
const { onFinishFailed } = this.callbacks;
if (onFinishFailed) {
onFinishFailed(e);
}
});
};
复制代码
解答一下this.callbacks
,callbacks
顾名思义,存储了在Form
中传递的回调函数,例如onFinish
、onFinishFailed
。对应的,useForm
拥有一个setCallbacks
方法。在初始化Form
的时候,把所需的事件存储,在正确的时机触发对应的回调。
// Form.tsx
import React from 'react';
import {
Store,
FormInstance,
Callbacks,
InternalFormInstance,
} from './interface';
import FieldContext, { HOOK_MARK } from './FieldContext';
import useForm from './useForm';
type BaseFormProps = Omit<
React.FormHTMLAttributes<HTMLFormElement>,
'onSubmit'
>;
type RenderProps = (
values: Store,
form: FormInstance,
) => JSX.Element | React.ReactNode;
const Form: React.FC<FormProps> = ({
name,
initialValues,
form,
children,
onValuesChange,
onFinish,
onFinishFailed,
...restProps
}: FormProps) => {
const [formInstance] = useForm(form);
const { setCallbacks } = formInstance as FormInstance;
setCallbacks({
onFinish,
onFinishFailed,
});
const mountRef = React.useRef<boolean>(true);
if (!mountRef.current) {
mountRef.current = false;
}
const WrapperNode = (
<FieldContext.Provider value={formInstance}>
{children}
</FieldContext.Provider>
);
return (
<form
{...restProps}
onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
event.stopPropagation();
formInstance.submit();
}}
>
{WrapperNode}
</form>
);
};
export default Form;
复制代码
目前的成果演示:
初始化值
// useForm
private initialValues: Store = {};
private setInitialValues = (initialValues: Store, init: boolean) => {
if (init) return; // 初始化组件的时候,才能有效设置初始值
this.initialValues = initialValues || {};
this.setFieldsValue(initialValues);
};
// 更新registerField方法,增加设置
private registerField = (entity: FieldEntity) => {
this.fieldEntities.push(entity);
const { name, initialValue } = entity.props;
// 设置Field的初始值
if (initialValue !== undefined && name) {
this.initialValues = {
...this.initialValues,
[name]: initialValue,
};
this.setFieldsValue({
...this.store,
[name]: initialValue,
});
}
// un-register field callback
return () => {
this.fieldEntities = this.fieldEntities.filter(item => item !== entity);
// this.store = setValue(this.store, namePath, undefined); // 删除移除字段的值
};
};
复制代码
以上useForm
的更新使其提供了设置表单initialValues
的函数,并且完成了Field
的initialValues
的功能。在Form.tsx
中来使用一下setInitialValues
:
// Form.tsx新增
const { setCallbacks, setInitialValues } = formInstance as FormInstance;
const mountRef = React.useRef<boolean>(true);
setInitialValues(initialValues || {}, !mountRef.current);
if (!mountRef.current) {
mountRef.current = false;
}
复制代码
重置表单
给useForm.tsx
新增方法:
private resetFields = (nameList?: string[]) => {
if (!nameList) {
this.store = { ...this.initialValues };
this.setFieldsValue(this.store, true);
}
};
// 更新setFieldsValue方法
// 增加reset参数,由重置方法触发时,触发所有组件的重新渲染
setFieldsValue = (newStore: any, reset?: boolean) => {
this.store = {
...this.store,
...newStore,
};
this.getFieldEntities(true).forEach(({ props, onStoreChange }) => {
const name = props.name as string;
Object.keys(newStore).forEach(key => {
if (name === key || reset) {
onStoreChange();
}
});
});
};
复制代码
值改变监听
对改变值的监听,在setFieldsValue
的时候可以比对获取改变的值和改变后的store
:
// useForm.tsx
private setFieldsValue = (values: any, reset?: boolean) => {
const nextStore = {
...this.store,
...values,
};
this.store = nextStore;
this.getFieldEntities(true).forEach(({ props, onStoreChange }) => {
const name = props.name as string;
Object.keys(values).forEach((key) => {
if (name === key || reset) {
onStoreChange();
}
});
});
// 从callbacks中获取到onValuesChange
const { onValuesChange } = this.callbacks;
if (onValuesChange) {
onValuesChange(values, nextStore);
}
};
// 记得要在Form.tsx中获取onValuesChange,并放入callbacks中
// Form.tsx
setCallbacks({
onFinish,
onFinishFailed,
onValuesChange,
});
复制代码
乞丐版表单到这里就完成了,来验收一下成果:
最后
本文参照rc-field-form
源码实现了一个简易版本的表单组件。阅读完本文之后,对于文章开头提出的4个问题,相信在你心里已经有了确定的答案,对rc-field-form
的核心原理也有一个清晰的思路。也感谢你能花时间阅读本文。
“难”绝对是生命中幸福的开始,“容易”绝对不是该庆幸的事。