Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rc-form源码解读 #86

Open
chencl1986 opened this issue Oct 7, 2019 · 0 comments
Open

rc-form源码解读 #86

chencl1986 opened this issue Oct 7, 2019 · 0 comments

Comments

@chencl1986
Copy link
Owner

阅读更多系列文章请访问我的GitHub博客,本文示例代码请访问这里

前言

在开发过程中,进行表单校验是一个很常用的功能。

表单校验通常需要实现以下几个功能:

  1. 收集各表单项的数据,如Input输入框,Select选择框等。

  2. 按照需求,对表单项数据进行校验,并显示校验结果。

  3. 需要提交表单时,对表单中所有数据进行校验,并收集所有数据。

这些功能看似简单,自己实现的话,还是会产生不少问题。因此,最好使用已有的库来实现此功能。我在开发中通常使用Ant DesignForm组件。从文档中的介绍可以看出,Form组件的功能实现主要是引用了rc-form

rc-form在开发中帮了我不少忙,我对它的实现方式也很感兴趣,于是研究了它的源码,现在与大家分享一下。

阅读本文你将得到什么

我将为你梳理rc-form的主要实现思路,为你讲解rc-form源码中部分方法的用途,并提供部分代码的注释。

最后,我还会自己实现一个精简版的rc-form组件,供你参考。

开始前准备

  1. 本文中的Demo使用TypeScript编写,如果你对TypeScript不了解,可以查看TypeScript文档。但只要有JavaScript基础,就不会影响阅读。

  2. 为了方便理解,你还可以从GitHub下载Ant Designrc-form的源码。

  3. 获取文中示例项目代码,请点击这里

Demo运行方法:

$ yarn install
$ yarn start		# visit http://localhost:3000/

运行后你会看到一个这样的Demo页面:

Demo

rc-form表单Demo

下图是一个rc-form表单Demo,你可以在http://localhost:3000/页面中,点击表单弹窗按钮,查看效果。

rc-form表单Demo

这个表单实现了如下功能:

  1. 在用户输入时和点击确认按钮时,会进行表单项的非空校验。

  2. 用户输入和选择的结果,会显示在表单下方。

  3. 点击确认按钮,若校验通过,会弹窗提示用户输入结果。

rc-form表单Demo实现代码

该表单是使用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用到的方法

在这个Demo中,我们主要用到了以下几个方法:

  1. Form.create:创建一个新的表单组件,提供表单校验、获取数据等方法,以及存储表单数据功能。

  2. this.props.form.getFieldDecorator:为表单字段绑定value和onChange等事件,并实现校验等功能。

  3. this.props.form.getFieldValue:获取表单字段值。

  4. this.props.form.setFieldsValue:为表单字段设置值。

  5. this.props.form.validateFields:进行表单校验,并返回校验结果和当前表单数据。

  6. this.props.form.resetFields:重置表单数据为初始值。

Form.create方法解读

上面列出的方法中,除了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,
  });
};

createDOMForm方法解读

查看rc-form源码,可以看到createDOMForm方法仅仅是调用了createBaseForm方法。

createDOMForm示例代码位置:/rc-form/src/createDOMForm.js

function createDOMForm(option) {
  return createBaseForm({
    ...option,
  }, [mixin]);
}

高阶组件(HOC)

createBaseForm的实现方式

现在我们的重点应当放在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)。

getFieldDecorator的实现方式

我们现在已经知道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)简介

高阶组件是一个获取组件并返回新组件的函数。

高阶组件(HOC)有以下特点:

  1. 高阶组件是对已有组件的封装,形成了一个新组件,新组件实现了特定的业务逻辑,并将其通过props传给原有组件。

  2. 高阶组件通常不需要实现UI,其UI由传入的原组件实现,它只是为原组件提供了额外的功能或数据。

高阶组件(HOC)Demo

下面来看一个简单的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属性,可以获取到原组件。

高阶组件(HOC)的简单使用

下面是一个使用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

高阶组件(HOC)的结合弹窗使用

下面是一个使用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

rc-form源码解读

阅读源码的意见

有了HOC的知识作为铺垫,我们就可以正式进入rc-form源码解读了。

开始正式的解读之前,我先说说我个人对于阅读源码的意见,以rc-form为例,它实现的功能虽然不是十分复杂,但由于这是一个要提供给很多人使用的库,为了避免出错,就需要进行很多的校验,以及开发环境提示等等。虽然这些都是必要的,但却会导致代码十分冗长,如果对代码不熟悉的话,会有不小的阅读障碍。

因此,我个人认为在阅读源码的时候,可以把重点放在以下两个方面:

  1. 理解作者进行开发的思路。就如同谈Redux的时候,都要了解的Flux架构。建议在阅读源码的时候,重点放在理解作者“为什么要这么做”,而不是研究作者是如何实现某个功能的。

  2. 学习作者的优秀习惯、技巧。上面说重点要理解作者的思路,但并不是让你放弃关注细节,而是要有取舍地看。一些自己完全有能力实现,或者作者只是在做一些报错提示之类的代码,可以直接跳过。当然如果看到作者的一些优秀习惯、技巧,或者是一些自己没有想过的实现方式,还是很有必要借鉴的。

rc-form的实现思路

我梳理了rc-form的实现思路,供大家参考,本次源码解读会按照下图进行讲解。建议你在查看rc-form源码时,时常对照这张图,这样更加便于理解。

rc-form的实现思路

在之前的高阶组件(HOC)讲解中,已经解读过createBaseForm方法的实现方式,这里就不再赘述。

接下来将依次以createBaseForm中的各个方法,讲解一下rc-form的实现逻辑,每段代码解读都会提供该段代码实现的主要功能,为方便理解,在代码中也提供了部分注释。

getInitialState方法解读

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解读

render方法实现了组装新组件需要的props属性与方法,并将其传入新组件,这是一个普通的高阶组件实现方式。

新组件的props主要来自于createBaseForm.jscreateForm.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)解读

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;
}

getFieldsValue/getFieldValue解读

createBaseForm.js中并未实现getFieldsValuegetFieldValue方法,而是直接调用了this.fieldsStore.getFieldsValuethis.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解读

setFieldsValue方法,实现的就是设置表单数据的功能,代码按如下流程调用:

this.setFieldsValuethis.setFieldsthis.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解读

resetFields方法,实现了重置表单为初始值功能。它的实现方式是:

  1. 先调用this.fieldsStore.resetFields方法,清空所有表单数据。

  2. 调用this.fieldsStore.setFields方法设置表单数据。因设置数据时并未传入新数据,默认会设置为fieldsMeta中存储的initialValue,以达到重置表单的目的。

示例代码位置:/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;
  }, {});
}

getFieldError解读

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;
}

validateFields解读

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的实现思路,以及部分常用方法的实现方式。

相信你已经发现,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

结语

在本文中,我围绕rc-form为你介绍了如下内容:

  1. rc-form表单Demo
  2. 高阶组件(HOC)的实现方式及应用Demo
  3. rc-form源码实现思路及提供部分代码注释
  4. 实现自己的rc-form

希望能够对你理解和使用rc-form有所帮助。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant