在开发过程中,进行表单校验是一个很常用的功能。
表单校验通常需要实现以下几个功能:
收集各表单项的数据,如Input输入框,Select选择框等。
按照需求,对表单项数据进行校验,并显示校验结果。
需要提交表单时,对表单中所有数据进行校验,并收集所有数据。
这些功能看似简单,自己实现的话,还是会产生不少问题。因此,最好使用已有的库来实现此功能。我在开发中通常使用Ant Design的Form组件。从文档中的介绍可以看出,Form组件的功能实现主要是引用了rc-form。
rc-form在开发中帮了我不少忙,我对它的实现方式也很感兴趣,于是研究了它的源码,现在与大家分享一下。
我将为你梳理rc-form的主要实现思路,为你讲解rc-form源码中部分方法的用途,并提供部分代码的注释。
最后,我还会自己实现一个精简版的rc-form组件,供你参考。
本文中的Demo使用TypeScript编写,如果你对TypeScript不了解,可以查看TypeScript文档。但只要有JavaScript基础,就不会影响阅读。
为了方便理解,你还可以从GitHub下载 Ant Design 和 rc-form 的源码。
获取文中示例项目代码,请点击 这里 。
Demo运行方法:
$ yarn install $ yarn start # visit http://localhost:3000/ 复制代码
运行后你会看到一个这样的Demo页面:
下图是一个rc-form表单Demo,你可以在 http://localhost:3000/ 页面中,点击 表单弹窗
按钮,查看效果。
这个表单实现了如下功能:
在用户输入时和点击确认按钮时,会进行表单项的非空校验。
用户输入和选择的结果,会显示在表单下方。
点击确认按钮,若校验通过,会弹窗提示用户输入结果。
该表单是使用Ant Design Form组件实现的,代码如下:
示例代码位置: /src/components/FormModal.tsx
import React from 'react' import {Form, Input, Select} from 'antd' import {FormComponentProps} from 'antd/lib/form' import FormItem, {FormItemProps} from 'antd/lib/form/FormItem' import Modal, {ModalProps} from 'antd/lib/modal' const Option = Select.Option // FormItem宽度兼容 export const formItemLayout: FormItemProps = { labelCol: { xs: {span: 24}, sm: {span: 6} }, wrapperCol: { xs: {span: 24}, sm: {span: 16} } } // 性别枚举 enum SexEnum { male = 'male', female = 'female' } // 性别名称枚举 enum SexNameEnum { male = '男', female = '女' } // 表单字段类型 export class FormModalValues { username: string = '' sex: SexEnum = SexEnum.male } export interface Props extends ModalProps, FormComponentProps { } export class State { visible: boolean = false } export class FormModalComponent extends React.Component<Props, State> { constructor(props: Props) { super(props) this.state = new State() } // 打开弹窗 public show = (): void => { this.setState({ visible: true }) } // 关闭弹窗 public hide = (): void => { this.setState({ visible: false }) } public onOk = async () => { // 方法1:使用回调函数获取表单验证结果 /* this.props.form.validateFields(async (errors: any, {username, sex}: FormModalValues) => { if (!errors) { Modal.success({ title: '表单输入结果', content: `用户名:${username},性别:${SexNameEnum[sex]}。` }) this.hide() } }) */ // 方法2:使用async函数获取表单验证结果 try { // @ts-ignore const {username, sex}: FormModalValues = await this.props.form.validateFields() Modal.success({ title: '表单输入结果', content: `用户名:${username},性别:${SexNameEnum[sex]}。` }) this.hide() } catch (error) { console.error(error) return error } } // 关闭弹窗后初始化弹窗参数 public afterClose = (): void => { // 重置表单 this.props.form.resetFields() this.setState(new State()) } componentDidMount() { // 为表单设置初始值,这里与getFieldDecorator方法中的initialValue重复。 this.props.form.setFieldsValue(new FormModalValues()) } render() { const form = this.props.form // 获取用户输入的表单数据 const username: string = form.getFieldValue('username') const sex: SexEnum = form.getFieldValue('sex') return ( <Modal visible={this.state.visible} title={'新建用户'} maskClosable onCancel={this.hide} onOk={this.onOk} afterClose={this.afterClose} > <FormItem label={'请输入用户名'} required={true} {...formItemLayout} > { // getFieldDecorator为表单字段绑定value和onChange等事件,并实现校验等功能 form.getFieldDecorator<FormModalValues>( // 表单项数据字段 'username', { // 表单初始值 initialValue: '', // 表单校验规则 rules: [ { required: true, } ] } )( <Input /> ) } </FormItem> <FormItem label={'请选择性别'} required={true} {...formItemLayout} > { // getFieldDecorator为表单字段绑定value和onChange等事件,并实现校验等功能 form.getFieldDecorator<FormModalValues>( // 表单项数据字段 'sex', { // 表单初始值 initialValue: SexEnum.male, // 表单校验规则 rules: [ { required: true, } ] } )( <Select style={{width: '60px'}} > <Option value={'male'} > 男 </Option> <Option value={'female'} > 女 </Option> </Select> ) } </FormItem> <FormItem label={'输入的用户名'} {...formItemLayout} > {username} </FormItem> <FormItem label={'选择的性别'} {...formItemLayout} > { SexNameEnum[sex] } </FormItem> </Modal> ) } } const FormModal = Form.create<Props>()(FormModalComponent) export default FormModal 复制代码
在这个Demo中,我们主要用到了以下几个方法:
Form.create:创建一个新的表单组件,提供表单校验、获取数据等方法,以及存储表单数据功能。
this.props.form.getFieldDecorator:为表单字段绑定value和onChange等事件,并实现校验等功能。
this.props.form.getFieldValue:获取表单字段值。
this.props.form.setFieldsValue:为表单字段设置值。
this.props.form.validateFields:进行表单校验,并返回校验结果和当前表单数据。
this.props.form.resetFields:重置表单数据为初始值。
上面列出的方法中,除了Form.create,都是rc-form提供的方法。
但如果查看create方法的实现方式,可以发现它直接调用了 rc-form
下的 createDOMForm
,如下:
示例代码位置: /ant-design/components/form/Form.tsx
import createDOMForm from 'rc-form/lib/createDOMForm'; 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, }); }; 复制代码
查看rc-form源码,可以看到 createDOMForm
方法仅仅是调用了 createBaseForm
方法。
createDOMForm示例代码位置: /rc-form/src/createDOMForm.js
function createDOMForm(option) { return createBaseForm({ ...option, }, [mixin]); } 复制代码
现在我们的重点应当放在 createBaseForm
方法上,不过它的代码足足有600多行,很难在短时间内弄清楚所有细节。
但我们只要理解 createBaseForm
的大体结构,就可以知道它主要完成了哪些功能。
以下是我简化过的 createBaseForm
代码:
createBaseForm示例代码位置: /rc-form/src/createBaseForm.js
function createBaseForm(option = {}, mixins = []) { return function decorate(WrappedComponent) { const Form = createReactClass({ render() { return <WrappedComponent {...props} /> } }) return Form } } export default createBaseForm 复制代码
从这段代码可以看出, createBaseForm
方法实际上就是实现了一个高阶组件(HOC)。
我们现在已经知道 createBaseForm
其实是一个高阶组件(HOC),那么再来看与之用法相似的 getFieldDecorator
方法,它的也是实现了一个高阶组件(HOC)`。
我简化过的 getFieldDecorator
代码如下:
getFieldDecorator示例代码位置: /rc-form/src/createBaseForm.js
getFieldDecorator(name, fieldOption) { const props = this.getFieldProps(name, fieldOption); return (fieldElem) => { return React.cloneElement(fieldElem, { ...props, }); }; } 复制代码
高阶组件(HOC)有以下特点:
高阶组件是对已有组件的封装,形成了一个新组件,新组件实现了特定的业务逻辑,并将其通过props传给原有组件。
高阶组件通常不需要实现UI,其UI由传入的原组件实现,它只是为原组件提供了额外的功能或数据。
下面来看一个简单的HOC例子:
示例代码位置:/src/utils/createTimer.tsx
import React from 'react' export interface Props { wrappedComponentRef?: React.RefObject<any> } export class State { time: Date = new Date() } export interface TimerProps { time: Date } function createTimer(WrappedComponent: React.ComponentClass<TimerProps>): React.ComponentClass<Props> { class Timer extends React.Component<Props, State> { timer: number = 0 constructor(props: Props) { super(props) this.state = new State() } componentDidMount() { this.timer = window.setInterval(() => { this.setState({ time: new Date() }) }, 1000) } componentWillUnmount() { clearInterval(this.timer) } render() { // 为原组件提供time的props后,将其作为组件返回显示,不对UI做修改 return ( <WrappedComponent ref={this.props.wrappedComponentRef} time={this.state.time} /> ) } } // 返回新组件 return Timer } export default createTimer 复制代码
这个例子实现了一个计时器的高阶组件,它将当前要显示的时间通过props中名为time的属性传入原组件。
同时,在返回的新组件中,可以通过设置wrappedComponentRef属性,可以获取到原组件。
下面是一个使用 createTimer
显示计时器的一个简单例子,该组件接收了HOC传过来的time属性,放入一个p标签中显示。
你可以在 http://localhost:3000/ 页面中, 表单弹窗
按钮下方看到显示的时间。
示例代码位置:/src/components/ShowTimer.tsx
import React from 'react' import moment from 'moment' import createTimer, {TimerProps} from '../utils/createTimer'; // 表单字段类型 export interface Props extends TimerProps { } export class State { } export class ShowTimerComponent extends React.Component<Props, State> { constructor(props: Props) { super(props) this.state = new State() } render() { return ( <p> {moment(this.props.time).format('YYYY-MM-DD HH:mm:ss')} </p> ) } } // 导出用HOC创建的新组件 const ShowTimer = createTimer(ShowTimerComponent) export default ShowTimer 复制代码
下面是一个使用 createTimer
创建弹窗显示计时器的例子,弹窗组件接收了HOC传过来的time属性,并将其显示出来。
同时将弹窗组件通过wrappedComponentRef属性提供给外部使用,实现了打开、关闭弹窗功能。
你可以在 http://localhost:3000/ 页面中,点击 时间弹窗
按钮,查看效果。
示例代码位置:/src/components/ShowTimerModal.tsx
import React from 'react' import moment from 'moment' import {Modal} from 'antd'; import {ModalProps} from 'antd/lib/modal'; import createTimer, {TimerProps} from '../utils/createTimer'; // 表单字段类型 export interface Props extends ModalProps, TimerProps { } export class State { visible: boolean = false } export class ShowTimerModalComponent extends React.Component<Props, State> { constructor(props: Props) { super(props) this.state = new State() } // 打开弹窗 public show = (): void => { this.setState({ visible: true }) } // 关闭弹窗 public hide = (): void => { this.setState({ visible: false }) } render() { return ( <Modal visible={this.state.visible} title={'弹窗显示时间'} maskClosable cancelButtonProps={{style: {display: 'none'}}} onCancel={this.hide} onOk={this.hide} > {moment(this.props.time).format('YYYY-MM-DD HH:mm:ss')} </Modal> ) } } // 导出用HOC创建的新组件 const ShowTimerModal = createTimer(ShowTimerModalComponent) export default ShowTimerModal 复制代码
有了HOC的知识作为铺垫,我们就可以正式进入rc-form源码解读了。
开始正式的解读之前,我先说说我个人对于阅读源码的意见,以rc-form为例,它实现的功能虽然不是十分复杂,但由于这是一个要提供给很多人使用的库,为了避免出错,就需要进行很多的校验,以及开发环境提示等等。虽然这些都是必要的,但却会导致代码十分冗长,如果对代码不熟悉的话,会有不小的阅读障碍。
因此,我个人认为在阅读源码的时候,可以把重点放在以下两个方面:
理解作者进行开发的思路。就如同谈Redux的时候,都要了解的Flux架构。建议在阅读源码的时候,重点放在理解作者“为什么要这么做”,而不是研究作者是如何实现某个功能的。
学习作者的优秀习惯、技巧。上面说重点要理解作者的思路,但并不是让你放弃关注细节,而是要有取舍地看。一些自己完全有能力实现,或者作者只是在做一些报错提示之类的代码,可以直接跳过。当然如果看到作者的一些优秀习惯、技巧,或者是一些自己没有想过的实现方式,还是很有必要借鉴的。
我梳理了rc-form的实现思路,供大家参考,本次源码解读会按照下图进行讲解。建议你在查看rc-form源码时,时常对照这张图,这样更加便于理解。
在之前的高阶组件(HOC)讲解中,已经解读过 createBaseForm
方法的实现方式,这里就不再赘述。
接下来将依次以 createBaseForm
中的各个方法,讲解一下rc-form的实现逻辑,每段代码解读都会提供该段代码实现的主要功能,为方便理解,在代码中也提供了部分注释。
createBaseForm
使用了 createReactClass
方法创建一个React组件类, getInitialState
主要用来为组件创建一些初始化参数、方法等,相当于ES6中的 constructor
。
从代码中可以看到, createFieldsStore
方法为该组件创建了存储、读取、设置表单数据等功能,并存储在 this.fieldsStore
属性中。
表单原始数据,如initialValue(表单项初始值)、rules(表单校验规则)等,都会存储在 this.fieldsStore.fieldsMeta
属性中。
当前的表单数据,会存储在 this.fieldsStore.fields
属性中。
示例代码位置: /rc-form/src/createBaseForm.js
getInitialState() { // option.mapPropsToFields: 将值从props转换为字段。用于从redux store读取字段。 const fields = mapPropsToFields && mapPropsToFields(this.props); // createFieldsStore为该组件提供了存储、读取、设置表单数据等功能 this.fieldsStore = createFieldsStore(fields || {}); this.instances = {}; this.cachedBind = {}; this.clearedFieldMetaCache = {}; this.renderFields = {}; this.domFields = {}; // 为组件绑定了一系列方法,这些方法会通过props传入新组件供其使用 // HACK: https://github.com/ant-design/ant-design/issues/6406 ['getFieldsValue', 'getFieldValue', 'setFieldsInitialValue', 'getFieldsError', 'getFieldError', 'isFieldValidating', 'isFieldsValidating', 'isFieldsTouched', 'isFieldTouched'].forEach(key => { this[key] = (...args) => { if (process.env.NODE_ENV !== 'production') { warning( false, 'you should not use `ref` on enhanced form, please use `wrappedComponentRef`. ' + 'See: https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140' ); } // 该组件中的方法,直接调用了fieldsStore中的方法,也就是由createFieldsStore方法创建的 return this.fieldsStore[key](...args); }; }); return { submitting: false, }; } 复制代码
render方法实现了组装新组件需要的props属性与方法,并将其传入新组件,这是一个普通的高阶组件实现方式。
新组件的props主要来自于 createBaseForm.js
和 createForm.js
中定义的mixin对象,如下面的代码:
示例代码位置: /rc-form/src/createForm.js
export const mixin = { getForm() { return { getFieldsValue: this.fieldsStore.getFieldsValue, getFieldValue: this.fieldsStore.getFieldValue, getFieldInstance: this.getFieldInstance, setFieldsValue: this.setFieldsValue, setFields: this.setFields, setFieldsInitialValue: this.fieldsStore.setFieldsInitialValue, getFieldDecorator: this.getFieldDecorator, getFieldProps: this.getFieldProps, getFieldsError: this.fieldsStore.getFieldsError, getFieldError: this.fieldsStore.getFieldError, isFieldValidating: this.fieldsStore.isFieldValidating, isFieldsValidating: this.fieldsStore.isFieldsValidating, isFieldsTouched: this.fieldsStore.isFieldsTouched, isFieldTouched: this.fieldsStore.isFieldTouched, isSubmitting: this.isSubmitting, submit: this.submit, validateFields: this.validateFields, resetFields: this.resetFields, }; }, }; 复制代码
也就是说,mixin定义了需要传递给新组件使用的方法。
示例代码位置: /rc-form/src/createBaseForm.js
render() { const {wrappedComponentRef, ...restProps} = this.props; // eslint-disable-line const formProps = { // getForm方法来自于createDOMForm方法调用createBaseForm方法时,传入的mixin对象 // mixin合并了createForm.js中导出的的mixin [formPropName]: this.getForm(), }; if (withRef) { if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { warning( false, '`withRef` is deprecated, please use `wrappedComponentRef` instead. ' + 'See: https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140' ); } formProps.ref = 'wrappedComponent'; } else if (wrappedComponentRef) { formProps.ref = wrappedComponentRef; } // 创建新组件的props const props = mapProps.call(this, { ...formProps, ...restProps, }); return <WrappedComponent {...props} />; }, }); 复制代码
getFieldDecorator
方法主要实现了一个高阶组件(HOC),它主要为新组件增加了绑定value属性和onChange事件,以及实现了onChange时的表单校验功能。
新组件的props是通过 getFieldProps
方法创建,该方法主要实现了绑定onChange事件,确保表单能够获取到表单项输入的值,在onChange的同时使用async-validator进行校验。
示例代码位置: /rc-form/src/createBaseForm.js
// 获取当前表单项的field、fieldMeta数据 onCollectCommon(name, action, args) { const fieldMeta = this.fieldsStore.getFieldMeta(name); if (fieldMeta[action]) { fieldMeta[action](...args); } else if (fieldMeta.originalProps && fieldMeta.originalProps[action]) { fieldMeta.originalProps[action](...args); } // 通过getValueFromEvent方法,从event中获取当前表单项的值,fieldMeta.getValueFromEvent为用户自定义的方法。 const value = fieldMeta.getValueFromEvent ? fieldMeta.getValueFromEvent(...args) : getValueFromEvent(...args); if (onValuesChange && value !== this.fieldsStore.getFieldValue(name)) { const valuesAll = this.fieldsStore.getAllValues(); const valuesAllSet = {}; valuesAll[name] = value; Object.keys(valuesAll).forEach(key => set(valuesAllSet, key, valuesAll[key])); onValuesChange({ [formPropName]: this.getForm(), ...this.props }, set({}, name, value), valuesAllSet); } // 获取相应字段的field数据 const field = this.fieldsStore.getField(name); return ({name, field: {...field, value, touched: true}, fieldMeta}); }, // 设置表单数据 onCollect(name_, action, ...args) { // 获取当前表单数据及设置 const {name, field, fieldMeta} = this.onCollectCommon(name_, action, args); const {validate} = fieldMeta; this.fieldsStore.setFieldsAsDirty(); const newField = { ...field, dirty: hasRules(validate), }; this.setFields({ [name]: newField, }); }, onCollectValidate(name_, action, ...args) { // 获取当前表单数据及设置 const {field, fieldMeta} = this.onCollectCommon(name_, action, args); const newField = { ...field, dirty: true, }; this.fieldsStore.setFieldsAsDirty(); // 进行表单校验,并存储表单数据 this.validateFieldsInternal([newField], { action, options: { firstFields: !!fieldMeta.validateFirst, }, }); }, // 返回一个表单项的onChange事件 getCacheBind(name, action, fn) { if (!this.cachedBind[name]) { this.cachedBind[name] = {}; } const cache = this.cachedBind[name]; if (!cache[action] || cache[action].oriFn !== fn) { cache[action] = { fn: fn.bind(this, name, action), oriFn: fn, }; } return cache[action].fn; }, // 创建新的表单项组件 getFieldDecorator(name, fieldOption) { // 注册表单项,获取新表单项的props,主要是value属性和onChange事件等 const props = this.getFieldProps(name, fieldOption); return (fieldElem) => { // We should put field in record if it is rendered this.renderFields[name] = true; const fieldMeta = this.fieldsStore.getFieldMeta(name); const originalProps = fieldElem.props; // 这段是在生产环境的打印提示语 if (process.env.NODE_ENV !== 'production') { const valuePropName = fieldMeta.valuePropName; warning( !(valuePropName in originalProps), `/`getFieldDecorator/` will override /`${valuePropName}/`, ` + `so please don't set /`${valuePropName}/` directly ` + `and use /`setFieldsValue/` to set it.` ); const defaultValuePropName = `default${valuePropName[0].toUpperCase()}${valuePropName.slice(1)}`; warning( !(defaultValuePropName in originalProps), `/`${defaultValuePropName}/` is invalid ` + `for /`getFieldDecorator/` will set /`${valuePropName}/`,` + ` please use /`option.initialValue/` instead.` ); } fieldMeta.originalProps = originalProps; fieldMeta.ref = fieldElem.ref; return React.cloneElement(fieldElem, { ...props, // 该方法用于返回当前表单存储的value值 ...this.fieldsStore.getFieldValuePropValue(fieldMeta), }); }; }, // 创建表单项组件的props getFieldProps(name, usersFieldOption = {}) { if (!name) { throw new Error('Must call `getFieldProps` with valid name string!'); } if (process.env.NODE_ENV !== 'production') { warning( this.fieldsStore.isValidNestedFieldName(name), `One field name cannot be part of another, e.g. /`a/` and /`a.b/`. Check field: ${name}` ); warning( !('exclusive' in usersFieldOption), '`option.exclusive` of `getFieldProps`|`getFieldDecorator` had been remove.' ); } delete this.clearedFieldMetaCache[name]; const fieldOption = { name, trigger: DEFAULT_TRIGGER, valuePropName: 'value', validate: [], ...usersFieldOption, }; const { rules, trigger, validateTrigger = trigger, validate, } = fieldOption; const fieldMeta = this.fieldsStore.getFieldMeta(name); if ('initialValue' in fieldOption) { fieldMeta.initialValue = fieldOption.initialValue; } const inputProps = { ...this.fieldsStore.getFieldValuePropValue(fieldOption), ref: this.getCacheBind(name, `${name}__ref`, this.saveRef), }; if (fieldNameProp) { inputProps[fieldNameProp] = formName ? `${formName}_${name}` : name; } // 获取表单项校验触发事件及校验规则 const validateRules = normalizeValidateRules(validate, rules, validateTrigger); // 获取表单项校验事件 const validateTriggers = getValidateTriggers(validateRules); validateTriggers.forEach((action) => { // 若已绑定了校验事件,则返回 if (inputProps[action]) return; // 绑定收集表单数据及校验事件 inputProps[action] = this.getCacheBind(name, action, this.onCollectValidate); }); // 若validateTriggers为空,则绑定普通事件,不进行校验 // 使用onCollect方法,获取绑定表单项输入值事件,将其存储到inputProps中,并返回给组件用作props // make sure that the value will be collect if (trigger && validateTriggers.indexOf(trigger) === -1) { // getCacheBind负责返回一个表单项的onChange事件 inputProps[trigger] = this.getCacheBind(name, trigger, this.onCollect); } // 将当前已设置的表单选项,与新表单选项合并,并存入fieldsMeta属性 const meta = { ...fieldMeta, ...fieldOption, validate: validateRules, }; // 注册表单项,将表单设置如initialValue、validateRules等,存储到this.fieldsStore.fieldsMeta[name]中 this.fieldsStore.setFieldMeta(name, meta); if (fieldMetaProp) { inputProps[fieldMetaProp] = meta; } if (fieldDataProp) { inputProps[fieldDataProp] = this.fieldsStore.getField(name); } // This field is rendered, record it this.renderFields[name] = true; return inputProps; }, 复制代码
示例代码位置: /rc-form/src/utils.js
/** * 将validate、rules、validateTrigger三个参数配置的校验事件及规则,整理成统一的校验事件、规则 * @param {Array<object>} validate 校验事件、规则 * @param {string} validate[].trigger 校验事件 * @param {object[]} validate[].rules 校验规则,参考async-validator,https://github.com/yiminghe/async-validator * @param {object[]} rules 校验规则,参考async-validator,https://github.com/yiminghe/async-validator * @param {string} validateTrigger 校验事件 * @returns {Array<object>} validateRules 校验事件、规则 * @returns {string[]} validateRules[].trigger 校验事件 * @returns {object[]} validateRules[].rules 校验规则,参考async-validator,https://github.com/yiminghe/async-validator */ export function normalizeValidateRules(validate, rules, validateTrigger) { const validateRules = validate.map((item) => { const newItem = { ...item, trigger: item.trigger || [], }; if (typeof newItem.trigger === 'string') { newItem.trigger = [newItem.trigger]; } return newItem; }); if (rules) { validateRules.push({ trigger: validateTrigger ? [].concat(validateTrigger) : [], rules, }); } return validateRules; } /** * 将validate、rules、validateTrigger三个参数配置的校验事件及规则,整理成统一的校验事件、规则 * @param {Array<object>} validateRules 校验事件、规则 * @param {string[]} validateRules[].trigger 校验事件 * @param {object[]} validateRules[].rules 校验规则,参考async-validator,https://github.com/yiminghe/async-validator * @returns {Array<string>} 校验事件 */ export function getValidateTriggers(validateRules) { return validateRules .filter(item => !!item.rules && item.rules.length) .map(item => item.trigger) .reduce((pre, curr) => pre.concat(curr), []); } // 判断表单项类型,获取表单数据,默认支持通过event.target.value或event.target.checked获取 export function getValueFromEvent(e) { // To support custom element if (!e || !e.target) { return e; } const {target} = e; return target.type === 'checkbox' ? target.checked : target.value; } 复制代码
createBaseForm.js
中并未实现 getFieldsValue
、 getFieldValue
方法,而是直接调用了 this.fieldsStore.getFieldsValue
、 this.fieldsStore.getFieldValue
方法,它们实现的功能是从存储的数据中,查找出指定的数据。
this.fieldsStore.getFieldsValue
方法如未指定需要查找的数据,则返回所有数据。
this.fieldsStore.getNestedField
是一个公用方法,根据传入的字段名,或者表单已存储的字段名,使用传入的回调函数获取所需的数据。
this.fieldsStore.getValueFromFields
方法,根据传入的字段名,获取当前表单的值,若值不存在,则返回已设置的initialValue。
示例代码位置: /rc-form/src/createFieldsStore.js
getFieldsValue = (names) => { return this.getNestedFields(names, this.getFieldValue); } getNestedFields(names, getter) { const fields = names || this.getValidFieldsName(); return fields.reduce((acc, f) => set(acc, f, getter(f)), {}); } getFieldValue = (name) => { const {fields} = this; return this.getNestedField(name, (fullName) => this.getValueFromFields(fullName, fields)); } // 从传入的fields中,按name获取相应的值,若没有则直接返回fieldMeta中设置的initialValue getValueFromFields(name, fields) { const field = fields[name]; if (field && 'value' in field) { return field.value; } const fieldMeta = this.getFieldMeta(name); return fieldMeta && fieldMeta.initialValue; } // 根据传入的name,获取fieldMeta中存在的字段名称,最终调用getter函数获取相应的值 getNestedField(name, getter) { const fullNames = this.getValidFieldsFullName(name); if ( fullNames.length === 0 || // Not registered (fullNames.length === 1 && fullNames[0] === name) // Name already is full name. ) { return getter(name); } const isArrayValue = fullNames[0][name.length] === '['; const suffixNameStartIndex = isArrayValue ? name.length : name.length + 1; return fullNames .reduce( (acc, fullName) => set( acc, fullName.slice(suffixNameStartIndex), getter(fullName) ), isArrayValue ? [] : {} ); } // 获取存储的表单字段名称 getValidFieldsFullName(maybePartialName) { const maybePartialNames = Array.isArray(maybePartialName) ? maybePartialName : [maybePartialName]; return this.getValidFieldsName() .filter(fullName => maybePartialNames.some(partialName => ( fullName === partialName || ( startsWith(fullName, partialName) && ['.', '['].indexOf(fullName[partialName.length]) >= 0 ) ))); } // 获取存储的表单字段名称 getValidFieldsName() { const {fieldsMeta} = this; // 过滤出fieldsMeta中存储的未被设置为hidden的数据 return fieldsMeta ? Object.keys(fieldsMeta).filter(name => !this.getFieldMeta(name).hidden) : []; } // 获取存储的字段数据 getFieldMeta(name) { this.fieldsMeta[name] = this.fieldsMeta[name] || {}; return this.fieldsMeta[name]; } 复制代码
setFieldsValue
方法,实现的就是设置表单数据的功能,代码按如下流程调用:
this.setFieldsValue
→ this.setFields
→ this.fieldsStore.setFields
最终新数据存储在 this.fieldsStore.fields
中。
示例代码位置: /rc-form/src/createBaseForm.js
setFieldsValue(changedValues, callback) { const {fieldsMeta} = this.fieldsStore; // 过滤出已注册的表单项的值 const values = this.fieldsStore.flattenRegisteredFields(changedValues); const newFields = Object.keys(values).reduce((acc, name) => { const isRegistered = fieldsMeta[name]; if (process.env.NODE_ENV !== 'production') { warning( isRegistered, 'Cannot use `setFieldsValue` until ' + 'you use `getFieldDecorator` or `getFieldProps` to register it.' ); } if (isRegistered) { const value = values[name]; acc[name] = { value, }; } return acc; }, {}); // 设置表单的值 this.setFields(newFields, callback); if (onValuesChange) { const allValues = this.fieldsStore.getAllValues(); onValuesChange({ [formPropName]: this.getForm(), ...this.props }, changedValues, allValues); } } setFields(maybeNestedFields, callback) { const fields = this.fieldsStore.flattenRegisteredFields(maybeNestedFields); this.fieldsStore.setFields(fields); if (onFieldsChange) { const changedFields = Object.keys(fields) .reduce((acc, name) => set(acc, name, this.fieldsStore.getField(name)), {}); onFieldsChange({ [formPropName]: this.getForm(), ...this.props }, changedFields, this.fieldsStore.getNestedAllFields()); } this.forceUpdate(callback); } 复制代码
示例代码位置: /rc-form/src/createFieldsStore.js
// 设置表单值 setFields(fields) { const fieldsMeta = this.fieldsMeta; // 将当前数据和传入的新数据合并 const nowFields = { ...this.fields, ...fields, }; const nowValues = {}; // 按照fieldsMeta中已注册的字段,从nowFields中取出这些字段的最新值,如果为空则设置为initialValue,形成新表单数据nowValues Object.keys(fieldsMeta) .forEach((f) => { nowValues[f] = this.getValueFromFields(f, nowFields); }); // 如果该表单项有设置normalize方法,则返回normalize之后的数据 // 可参考如这个例子:https://codepen.io/afc163/pen/JJVXzG?editors=001 Object.keys(nowValues).forEach((f) => { const value = nowValues[f]; const fieldMeta = this.getFieldMeta(f); if (fieldMeta && fieldMeta.normalize) { const nowValue = fieldMeta.normalize(value, this.getValueFromFields(f, this.fields), nowValues); if (nowValue !== value) { nowFields[f] = { ...nowFields[f], value: nowValue, }; } } }); this.fields = nowFields; } 复制代码
resetFields
方法,实现了重置表单为初始值功能。它的实现方式是:
this.fieldsStore.resetFields this.fieldsStore.setFields
示例代码位置: /rc-form/src/createBaseForm.js
resetFields(ns) { // 获取清空了所有fields中存储数据的对象 const newFields = this.fieldsStore.resetFields(ns); if (Object.keys(newFields).length > 0) { // 为newFields中的各个字段赋值,由于数据都为空,则会从fieldsMeta中查找initialValue赋值。 this.setFields(newFields); } if (ns) { const names = Array.isArray(ns) ? ns : [ns]; names.forEach(name => delete this.clearedFieldMetaCache[name]); } else { this.clearedFieldMetaCache = {}; } } 复制代码
示例代码位置: /rc-form/src/createFieldsStore.js
resetFields(ns) { const {fields} = this; // 获取需要重置的字段名称 const names = ns ? this.getValidFieldsFullName(ns) : this.getAllFieldsName(); // 如果当前fields中存在数据,则清空。最终返回的是清空了所有现有数据的对象。 return names.reduce((acc, name) => { const field = fields[name]; if (field && 'value' in field) { acc[name] = {}; } return acc; }, {}); } 复制代码
this.getFieldError
用于获取传入表单项的校验结果,包括校验的属性名称和提示语。
this.getFieldError
的调用方式与 this.getFieldValue
类似,最终也是通过调用 this.fieldsStore.getNestedField
方法,同时传入相应的回调函数,获取到需要的校验结果。
示例代码位置: /rc-form/src/createBaseForm.js
// 获取校验结果,返回的是校验错误提示语的数组,格式为string[] getFieldError = (name) => { return this.getNestedField( name, (fullName) => getErrorStrs(this.getFieldMember(fullName, 'errors')) ); } // 根据传入的name,获取fieldMeta中存在的字段名称,最终调用getter函数获取相应的值 getNestedField(name, getter) { const fullNames = this.getValidFieldsFullName(name); if ( fullNames.length === 0 || // Not registered (fullNames.length === 1 && fullNames[0] === name) // Name already is full name. ) { return getter(name); } const isArrayValue = fullNames[0][name.length] === '['; const suffixNameStartIndex = isArrayValue ? name.length : name.length + 1; return fullNames .reduce( (acc, fullName) => set( acc, fullName.slice(suffixNameStartIndex), getter(fullName) ), isArrayValue ? [] : {} ); } // 从当前字段数据中,获取传入member类型的数据 getFieldMember(name, member) { return this.getField(name)[member]; } // 获取存储的字段数据及校验结果,并补充字段名称 getField(name) { return { ...this.fields[name], name, }; } 复制代码
示例代码位置: /rc-form/src/utils.js
// 将错误数据整理后返回,返回的是校验错误提示语的数组,格式为string[] function getErrorStrs(errors) { if (errors) { return errors.map((e) => { if (e && e.message) { return e.message; } return e; }); } return errors; } 复制代码
this.validateFields
实现的是,先过滤出有配置rules校验规则的表单项,调用 async-validator
进行校验,并返回校验结果。
示例代码位置: /rc-form/src/createBaseForm.js
// 校验表单方法 validateFieldsInternal(fields, { fieldNames, action, options = {}, }, callback) { const allRules = {}; const allValues = {}; const allFields = {}; const alreadyErrors = {}; // 先清空已有的校验结果 fields.forEach((field) => { const name = field.name; if (options.force !== true && field.dirty === false) { if (field.errors) { set(alreadyErrors, name, {errors: field.errors}); } return; } const fieldMeta = this.fieldsStore.getFieldMeta(name); const newField = { ...field, }; newField.errors = undefined; newField.validating = true; newField.dirty = true; allRules[name] = this.getRules(fieldMeta, action); allValues[name] = newField.value; allFields[name] = newField; }); // 设置清空后的表单校验结果 this.setFields(allFields); // in case normalize Object.keys(allValues).forEach((f) => { allValues[f] = this.fieldsStore.getFieldValue(f); }); if (callback && isEmptyObject(allFields)) { callback(isEmptyObject(alreadyErrors) ? null : alreadyErrors, this.fieldsStore.getFieldsValue(fieldNames)); return; } // 使用AsyncValidator进行校验,并返回校验结果 const validator = new AsyncValidator(allRules); if (validateMessages) { validator.messages(validateMessages); } validator.validate(allValues, options, (errors) => { const errorsGroup = { ...alreadyErrors, }; // 如果校验不通过,则整理AsyncValidator返回的数据,并存储到表单数据中 if (errors && errors.length) { errors.forEach((e) => { const errorFieldName = e.field; let fieldName = errorFieldName; // Handle using array validation rule. // ref: https://github.com/ant-design/ant-design/issues/14275 Object.keys(allRules).some((ruleFieldName) => { const rules = allRules[ruleFieldName] || []; // Exist if match rule if (ruleFieldName === errorFieldName) { fieldName = ruleFieldName; return true; } // Skip if not match array type if (rules.every(({type}) => type !== 'array') && errorFieldName.indexOf(ruleFieldName) !== 0) { return false; } // Exist if match the field name const restPath = errorFieldName.slice(ruleFieldName.length + 1); if (/^/d+$/.test(restPath)) { fieldName = ruleFieldName; return true; } return false; }); const field = get(errorsGroup, fieldName); if (typeof field !== 'object' || Array.isArray(field)) { set(errorsGroup, fieldName, {errors: []}); } const fieldErrors = get(errorsGroup, fieldName.concat('.errors')); fieldErrors.push(e); }); } const expired = []; const nowAllFields = {}; Object.keys(allRules).forEach((name) => { const fieldErrors = get(errorsGroup, name); const nowField = this.fieldsStore.getField(name); // avoid concurrency problems if (!eq(nowField.value, allValues[name])) { expired.push({ name, }); } else { nowField.errors = fieldErrors && fieldErrors.errors; nowField.value = allValues[name]; nowField.validating = false; nowField.dirty = false; nowAllFields[name] = nowField; } }); // 存储新表单数据及结果 this.setFields(nowAllFields); if (callback) { if (expired.length) { expired.forEach(({name}) => { const fieldErrors = [{ message: `${name} need to revalidate`, field: name, }]; set(errorsGroup, name, { expired: true, errors: fieldErrors, }); }); } callback(isEmptyObject(errorsGroup) ? null : errorsGroup, this.fieldsStore.getFieldsValue(fieldNames)); } }); }, // 校验表单方法,主要用于整理需要校验的表单项数据后,调用validateFieldsInternal进行校验 validateFields(ns, opt, cb) { const pending = new Promise((resolve, reject) => { // 因传入的3个参数都为可选,需要将它们整理成固定的names, options, callback参数。 const {names, options} = getParams(ns, opt, cb); let {callback} = getParams(ns, opt, cb); if (!callback || typeof callback === 'function') { const oldCb = callback; callback = (errors, values) => { if (oldCb) { oldCb(errors, values); } else if (errors) { reject({errors, values}); } else { resolve(values); } }; } const fieldNames = names ? this.fieldsStore.getValidFieldsFullName(names) : this.fieldsStore.getValidFieldsName(); // 获取需要校验的表单项 const fields = fieldNames // 过滤出已配置rules的字段 .filter(name => { const fieldMeta = this.fieldsStore.getFieldMeta(name); return hasRules(fieldMeta.validate); }) // 获取当前表单数据 .map((name) => { const field = this.fieldsStore.getField(name); field.value = this.fieldsStore.getFieldValue(name); return field; }); if (!fields.length) { callback(null, this.fieldsStore.getFieldsValue(fieldNames)); return; } if (!('firstFields' in options)) { options.firstFields = fieldNames.filter((name) => { const fieldMeta = this.fieldsStore.getFieldMeta(name); return !!fieldMeta.validateFirst; }); } // 调用表单校验方法,进行校验 this.validateFieldsInternal(fields, { fieldNames, options, }, callback); }); pending.catch((e) => { if (console.error && process.env.NODE_ENV !== 'production') { console.error(e); } return e; }); return pending; }, 复制代码
在上一小节,我已经为你梳理了rc-form的实现思路,以及部分常用方法的实现方式。
相信你已经发现,rc-form的实现思路其实不复杂,分别使用HOC为新表单和表单项提供了所需要的扩展方法。
而 createFieldsStore.js
主要是为了实现这些拓展方法,而这些实现较为分复杂,虽然都是必要的,但确实对理解代码造成了一些障碍。
在阅读的时候,可以不必要过于拘泥于其中的细节,其实只要理解了使用HOC进行封装这一点,即使不理解 createFieldsStore.js
中的具体实现方式,也足够指导我们按照自己的思路来实现rc-form了。
我根据分析的rc-form实现思路,自己实现了一个rc-form功能,你可以在 http://localhost:3000/ 页面中,点击 新表单弹窗
按钮,查看效果。
实现rc-form示例代码位置: /src/utils/createForm.tsx
import React from 'react' import {observer} from 'mobx-react'; import {observable, runInAction, toJS} from 'mobx'; import hoistStatics from 'hoist-non-react-statics'; import AsyncValidator, {Rules, ValidateError, ErrorList, RuleItem} from 'async-validator'; // setFieldsValue设置表单数据时传入的数据类型 export class Values { [propName: string]: any } // 表单项设置 export class FieldOption { initialValue?: any rules: RuleItem[] = [] } // 表单项数据 export class Field { value: any errors: ErrorList = [] } // 表格数据 export class Fields { [propName: string]: Field } // 表单项设置数据 export class FieldMeta { name: string = '' fieldOption: FieldOption = new FieldOption() } // 表格设置数据 export class FieldsMeta { [propName: string]: FieldMeta } export interface Props { wrappedComponentRef: React.RefObject<React.Component<FormComponentProps, any, any>> } // 为原组件添加的form参数 export interface FormProps { getFieldDecorator: (name: string, fieldOption: FieldOption) => (fieldElem: React.ReactElement) => React.ReactElement getFieldValue: (name: string) => any setFieldsValue: (values: any) => void getFieldsValue: () => any validateFields: (callback?: (errors: any, values: any) => void) => void resetFields: () => void getFieldError: (name: string) => ErrorList } // 为原组件添加的props export interface FormComponentProps { form: FormProps } export class State { } function createForm(WrappedComponent: React.ComponentClass<FormComponentProps>): React.ComponentClass<Props> { @observer class Form extends React.Component<Props, State> { // 表单数据 @observable private fields: Fields = new Fields() // 表单原始数据 @observable private fieldsMeta: FieldsMeta = new FieldsMeta() constructor(props: Props) { super(props) this.state = new State() } // 创建表单项的props,提供给getFieldDecorator绑定事件 private getFieldProps = ( name: string, fieldOption: FieldOption = new FieldOption() ): any => { const initialValue = fieldOption.initialValue runInAction(() => { if (!this.fields[name]) { this.fields[name] = new Field() if (initialValue) { this.fields[name].value = initialValue } } if (!this.fieldsMeta[name]) { this.fieldsMeta[name] = { name, fieldOption } } }) return { value: toJS(this.fields)[name].value, onChange: (event: React.ChangeEvent<HTMLInputElement> | string): void => { if (typeof event === 'string') { this.fields[name].value = event } else { this.fields[name].value = event.target.value } this.forceUpdate() this.validateField(name) } } } // 创建新表单项组件的HOC private getFieldDecorator = ( name: string, fieldOption: FieldOption = new FieldOption() ): (fieldElem: React.ReactElement) => React.ReactElement => { const props = this.getFieldProps(name, fieldOption) return (fieldElem: React.ReactElement): React.ReactElement => { return React.cloneElement( fieldElem, props ) } } // 获取表单项数据 private getFieldValue = (name: string): any => { const field = toJS(this.fields)[name] return field && field.value } // 获取所有表单数据 private getFieldsValue = (): Values => { const fields = toJS(this.fields) let values: Values = {} Object.keys(fields).forEach((name: string): void => { values[name] = fields[name] }) return values } // 设置表单项的值 private setFieldsValue = (values: Values): void => { const fields = toJS(this.fields) Object.keys(values).forEach((name: string): void => { fields[name].value = values[name] }) this.fields = fields } // 获取用于表单校验的值和规则 private getRulesValues = (name?: string): {rules: Rules, values: Fields} => { const fields = toJS(this.fields) const fieldsMeta = toJS(this.fieldsMeta) const fieldMetaArr: FieldMeta[] = name ? [fieldsMeta[name]] : Object.values(fieldsMeta) const values: Fields = new Fields() const rules: Rules = fieldMetaArr.reduce((rules: Rules, item: FieldMeta): Rules => { if (item.fieldOption.rules.length) { values[item.name] = fields[item.name].value return { ...rules, [item.name]: item.fieldOption.rules } } return rules }, {}) return {rules, values} } // 校验单个表单项 private validateField = (name: string): void => { const {rules, values} = this.getRulesValues(name) const validator = new AsyncValidator(rules) validator.validate(values, {}, (errors: ErrorList): void => { this.fields[name].errors = [] if (errors) { errors.forEach((error: ValidateError): void => { this.fields[name].errors.push(error) }) } }) } // 校验整个表单 private validateFields = (callback?: (errors: ErrorList | null, values: Fields) => void): void => { const {rules, values} = this.getRulesValues() const validator = new AsyncValidator(rules) validator.validate(values, {}, (errors: ErrorList): void => { Object.keys(values).forEach((name: string): void => { this.fields[name].errors = [] }) if (errors) { errors.forEach((error: ValidateError): void => { this.fields[error.field].errors.push(error) }) } callback && callback(errors, values) }) // 强制渲染组件,避免 this.forceUpdate() } // 重置表单 private resetFields = (): void => { this.fields = Object.values(toJS(this.fieldsMeta)).reduce((fields: Fields, item: FieldMeta): Fields => { fields[item.name] = new Field() fields[item.name].value = item.fieldOption.initialValue return fields }, new Fields()) } // 获取表单项的校验结果 private getFieldError = (name: string): ErrorList => { return this.fields[name] ? this.fields[name].errors : [] } render() { let props: FormComponentProps = { form: { getFieldDecorator: this.getFieldDecorator, getFieldValue: this.getFieldValue, getFieldsValue: this.getFieldsValue, setFieldsValue: this.setFieldsValue, validateFields: this.validateFields, resetFields: this.resetFields, getFieldError: this.getFieldError, } } return ( <WrappedComponent ref={this.props.wrappedComponentRef} {...props} /> ) } } // 使用hoist-non-react-statics库,复制所有静态方法,请查看: // https://github.com/mridgway/hoist-non-react-statics // https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over return hoistStatics(Form, WrappedComponent) } export default createForm 复制代码
使用方法示例代码位置: /src/utils/NewFormModal.tsx
import React from 'react' import {Input, Select} from 'antd' import FormItem, {FormItemProps} from 'antd/lib/form/FormItem' import Modal, {ModalProps} from 'antd/lib/modal' import createForm, {FormComponentProps, FormProps} from '../utils/createForm' import {ErrorList, ValidateError} from 'async-validator' const Option = Select.Option // FormItem宽度兼容 export const formItemLayout: FormItemProps = { labelCol: { xs: {span: 24}, sm: {span: 6} }, wrapperCol: { xs: {span: 24}, sm: {span: 16} } } // 性别枚举 enum SexEnum { male = 'male', female = 'female' } enum SexNameEnum { male = '男', female = '女' } // 表单字段类型 export class FormModalValues { username: string = '' sex: SexEnum = SexEnum.male } export interface Props extends ModalProps, FormComponentProps { } export class State { visible: boolean = false } export class NewFormModalComponent extends React.Component<Props, State> { constructor(props: Props) { super(props) this.state = new State() } // 打开弹窗 public show = (): void => { this.setState({ visible: true }) } // 关闭弹窗 public hide = (): void => { this.setState({ visible: false }) } // 点击确认按钮 public onOk = () => { // 读取当前表单数据 const values: FormModalValues = this.props.form.getFieldsValue() console.log(values) this.props.form.validateFields((errors: any, {username, sex}: FormModalValues) => { if (!errors) { Modal.success({ title: '表单输入结果', content: `用户名:${username},性别:${SexNameEnum[sex]}。` }) this.hide() } }) } // 关闭弹窗后初始化弹窗参数 public afterClose = (): void => { this.props.form.resetFields() this.setState(new State()) } componentDidMount() { this.props.form.setFieldsValue(new FormModalValues()) } render() { const visible = this.state.visible const form: FormProps = this.props.form const username = form.getFieldValue('username') const sex: SexEnum = form.getFieldValue('sex') const usernameError: ErrorList = form.getFieldError('username') const sexError: ErrorList = form.getFieldError('sex') return ( <Modal visible={visible} title={'新建用户'} onCancel={this.hide} onOk={this.onOk} afterClose={this.afterClose} > <FormItem label={'请输入用户名'} required={true} validateStatus={usernameError.length ? 'error' : undefined} help={usernameError.length ? usernameError.map((item: ValidateError) => item.message).join(',') : undefined} {...formItemLayout} > { form.getFieldDecorator( 'username', { initialValue: '', rules: [ { required: true, } ] } )( <Input /> ) } </FormItem> <FormItem label={'请选择性别'} required={true} validateStatus={sexError.length ? 'error' : undefined} help={sexError.length ? sexError.map((item: ValidateError) => item.message).join(',') : undefined} {...formItemLayout} > { form.getFieldDecorator( 'sex', { initialValue: SexEnum.male, rules: [ { required: true, } ] } )( <Select style={{width: '60px'}} > <Option value={'male'} > 男 </Option> <Option value={'female'} > 女 </Option> </Select> ) } </FormItem> <FormItem label={'输入的用户名'} {...formItemLayout} > {username} </FormItem> <FormItem label={'选择的性别'} {...formItemLayout} > { SexNameEnum[sex] } </FormItem> </Modal> ) } } const NewFormModal = createForm(NewFormModalComponent) export default NewFormModal 复制代码