Form 表單在數棧的應用(下):深入篇

語言: CN / TW / HK

這篇文章的主題為我們對 Form 表單在數棧產品中使用之後理解消化的一個過程,通過介紹一些 Form 表單中常用到的方法,來理解部分設計思想,加深我們對技術的追求。主要介紹 Form 表單的建立和 Form表單雙向繫結(getFieldDecorator)。

後文中所提到的 Form 表單均為 Antd 3.x 中的 Form 元件,以下簡稱為 Form 表單。在 Form 表單在數棧的應用(上): 校驗篇 中提到,我們生在一個最好的時代,其實是別人造好輪子幫我們做了一些事情,那我們今天看一看,別人的輪子是怎麼造的,我們自己能不能實現。 留心過 Antd 的同學可能有印象,Antd 是基於 react-component 元件進行了 UI 封裝,文章會以 react-component/form 的程式碼為主。

一、別人的 Form

1.1 From.create

先檢視 createForm.js 檔案,該檔案主要是對 createBaseForm.js 檔案進行了一層封裝,並加上一些常用的方法。

import createBaseForm from './createBaseForm';
​
export const mixin = {
  getForm() {
    return {
      getFieldsValue: this.fieldsStore.getFieldsValue,
      getFieldValue: this.fieldsStore.getFieldValue,
      ...
      validateFields: this.validateFields,
      resetFields: this.resetFields,
    };
  },
};
​
function createForm(options) {
  return createBaseForm(options, [mixin]);
}
​
export default createForm;

接下來檢視一下 createBaseForm.js 檔案,主要檢視該檔案中的 createBaseForm 方法,這個方法起到裝飾器的作用,在 props 中包裝了一個預設為 form 的變數,在這個變數中完成 Form 的所有功能。createBaseForm 的作用是拷貝當前傳遞來的元件,也就是呼叫函式將當前元件傳遞下去作為被包裝元件,最終返回一個被包裝過的具備新屬性的元件。

render() {
  const { wrappedComponentRef, ...restProps } = this.props; // eslint-disable-line
  const formProps = {
    // getForm 方法來自 createForm.js,在 props 中包裝一個 formPropName 變數,預設為 form
    [formPropName]: this.getForm(),
  };
  // 獲取 form 的例項
  if (withRef) {
    formProps.ref = 'wrappedComponent';
  } else if (wrappedComponentRef) {
    formProps.ref = wrappedComponentRef;
  }
  const props = mapProps.call(this, {
    ...formProps,
    ...restProps,
  });
  return <WrappedComponent {...props} />;
}

裝飾器(decorator): 是一種與 相關的語法,主要用來修改類和類方法(類屬性),大部分面向物件的程式語言都支援這種語法,比如Java、Python。裝飾器可以簡單理解為:能對一些 物件 進行修改,然後返回一個被包裝過的 物件

綜合來看,Form.create(options) 實際上是對我們的業務元件進行了一次封裝,進行了 Form 相關屬性的初始化,掛載了一些需要使用的方法,並將這些方法新增到 props.form 下。

1.2 getFieldDecorator

<FormItem {...formItemLayout} label="姓 名" >
  {getFieldDecorator('name', {
    initialValue: userInfo.name,
    rules: [
      { required: true, message: '姓名不可為空!' }
    ]
  })(
    <Input placeholder="請輸入姓名" />
  )}
</FormItem>

從上述使用程式碼和下方實現方法可以看出,getFieldDecorator 是一個柯里化的函式,通過 id 和引數的輸入,返回以輸入元件為入參加上新屬性的一個 Dom 節點,把 option 的valuePropName、getValueProps、initialValue、rules 等各種 props 掛載到輸入元件上。

getFieldDecorator(name, fieldOption) {
  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;
    fieldMeta.originalProps = originalProps;
    fieldMeta.ref = fieldElem.ref;
    const decoratedFieldElem = React.cloneElement(fieldElem, {
      ...props,
      // 沒有 initialValue 時為 undefined,有則是 initialValue 的值
      ...this.fieldsStore.getFieldValuePropValue(fieldMeta),
    });
    return supportRef(fieldElem) ? (
      decoratedFieldElem
    ) : (
      <FieldElemWrapper name={name} form={this}>
        {decoratedFieldElem}
      </FieldElemWrapper>
    );
  };
}

getFieldDecorator 有以下兩個作用,可在 createBaseForm.js 檔案的 getFieldPropsgetFieldValuePropValue 方法中分別驗證:

  • 在初始化資料欄位時將資料欄位放到 fieldsStore 中;
  • 掛載 props 到輸入元件上時會從 fieldsStore中讀取資料欄位。

1.3 validateFields

通常使用 validateFields 方法對我們的表單資料進行校驗,檢視 createBaseForm.js 檔案中 validateFields 方法的實現後,發現 validateFields 方法返回一個 Promise 並且拼裝 validateFieldsInternal 方法需要的引數。

validateFields(ns, opt, cb) {
  const pending = new Promise((resolve, reject) => {
    ...
    this.validateFieldsInternal(..., params, callback);
  });
  ...
  return pending;
}

再看 validateFieldsInternal 方法的程式碼,它會從 fieldsStore 中獲取 rules 和資料 fields 的值,校驗後將錯誤資訊分別儲存到對應的 fieldsStore 中。

import AsyncValidator from 'async-validator';
​
validateFieldsInternal(
  fields,
  { fieldNames, action, options = {} },
  callback,
) {
  const fieldMeta = this.fieldsStore.getFieldMeta(name);
  ...
  const validator = new AsyncValidator(allRules);
  validator.validate(allValues, options, errors => {
    if (errors && errors.length) {
      errors.forEach(e => {
        ...
        const fieldErrors = get(errorsGroup, fieldName.concat('.errors'));
        fieldErrors.push(e);
      });
    }
  });
  ...
  this.setFields(nowAllFields);
  ...
}

總得來說,Form 表單從初始化到表單收集校驗經過了以下幾個步驟: 1、通過 Form.create 方法初始了一些屬性到 props.form 中,供開發者呼叫; 2、通過 getFieldDecorator 初始化表單的屬性和值,達到雙向繫結的效果; 3、校驗通過,把資料存到 fieldsStore 中;校驗不通過,把 error 存到 fieldsStore 中,渲染。

二、自己的 Form

效果和程式碼可以在 https://stackblitz.com/edit/react-ts-uoj5pj 檢視。 在這裡插入圖片描述

2.1 getFieldDecorator

/**
 * 實現 getFieldDecorator 方法
 * 初始化時將 initialValue 賦值給輸入框的 value
 * 輸入框變化時可以拿到 value
 */
const getFieldDecorator = (key: string, options: any) => {
  // 判斷是否第一次賦值,避免死迴圈
  const first = Object.keys(formData).indexOf(key) === -1;
​
  if (options.rules) {
    rules[key] = [...options.rules];
  }
  if (first && options.initialValue) {
    setFormData({ ...formData, [key]: options.initialValue });
  }
​
  return (formItem) => {
    if (errObj[key]) {
      formItem = {
        ...formItem,
        props: { ...formItem.props, className: 'input err' },
      };
    }
    return (
      <div className="form-item">
        {React.cloneElement(formItem, {
          name: key,
          value: formData[key] || '',
          onChange: (e: any) => {
            // 輸入框值變化時去除錯誤提示
            setErrObj({ ...errObj, [key]: '' });
            setFormData({ ...formData, [key]: e.target.value });
          },
          onBlur: () => {
            // 當前預設 blur 時進行校驗
            validateFields();
          },
        })}
        <div className="err-text">{errObj[key] || ' '}</div>
      </div>
    );
  };
};

2.2 validateFields

// 繫結校驗方法
const validateFields = (cb?: any) => {
  let errObjTemp = {};
  Object.keys(rules).forEach((key) => {
    rules[key].forEach((rule) => {
      if (rule?.required && (!formData[key] || formData[key].trim() === '')) {
        errObjTemp = {
          ...errObjTemp,
          [key]: rule?.message || `${key}為必填項!`,
        };
        setErrObj(errObjTemp);
      }
    });
  });
  cb && cb(Object.keys(errObjTemp).length ? errObjTemp : undefined, formData);
};

2.3 createForm

const createForm = (FormFunc) => (props) => {
  const [formData, setFormData] = useState({});
  const [errObj, setErrObj] = useState({});
  const rules = {};
  
  ...
  
  // 將自定義方法掛載到 props 上
  return FormFunc({ ...props, getFieldDecorator, validateFields });
};