rc-form源碼解讀

閱讀更多系列文章請訪問我的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有所幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章