一個 React Form 組件的重構思路

本文發佈於 個人博客

最近對團隊內部 React 組件庫(ne-rc)中的 Form 組件進行了重構,記錄一下思考的過程。react

一些前置定義:git

名詞 定義
表單 Form 組件
子表單 嵌套在 Form 下面的相似 Input, Select 這樣的子組件

首先咱們看一下,咱們的對 Form 組件的需求是什麼。github

  1. 獲取當前變更表單的狀態異步

    • 校驗全部必填表單是否填寫完成
    • 對外觸發具體表單變化的方法 formFieldChange
  2. 暴露對外提供整個表單狀態的方法this

    • 提供整個表單最新狀態的方法 $Form.data
  3. 提交方法code

    • 校驗表單是否經過校驗
    • 對外觸發 formSubmit 方法

接着咱們從重構前和重構後,看如何來解決這個問題。orm

Before

獲取當前變更表單的狀態

如何獲取變更的子表單

React 父子通訊須要經過 prop 傳遞方法,對於 Form 下面的相似與 Input 之類的子表單的變化想要通知到父級,若是不借助第三方的事件傳遞方法,那麼就只能經過由父級經過 props 向 Input 傳遞 formFieldChange(假設就叫這個名字)方法,而後當子組件變化時去調用 formFieldChange 來實現。對象

那麼問題來了,何時去傳遞這個方法呢?遞歸

不能在具體頁面裏面使用的時候再去每條表單裏面註冊這個方法,那每一個用到表單組件的時候就都須要給子表單進行這樣的事件綁定,這樣太累了。接口

因此一開始,我選擇經過直接遞歸的遍歷 Form 下面的 children,只要發現這個 children 是我想要的表單類型,那麼就從新克隆一個帶有 formFieldChange 的組件來替換掉原來的組件。

/**
  * 獲取 form 下面每個表單對象,注入屬性,並收集起來
  * @param children
  * @returns {*}
  */
function getForms(children) {
  return React.Children.map(children, (el, i) => {
    if (!el) {
      return null
    }
    switch (el.type) {
      case Input:
        Forms.push(el)
        return React.cloneElement(
          el,
          {
            key: i,
            formFieldChange,
            emptyInput
          }
        )
      case Select:
        Forms.push(el)
        return React.cloneElement(
          el,
          {
            key: i,
            formFieldChange
          }
        )
      case CheckBox:
        Forms.push(el)
        return React.cloneElement(
          el,
          {
            key: i,
            formFieldChange
          }
        )
      default:
        if (el.props && el.props.children instanceof Array) {
          const children = getForms(el.props.children)
          return React.cloneElement(
            el,
            {
              key: i,
              children
            }
          )
        } else {
          return el
        }
    }
  })
}

這樣,全部的特定子組件就均可以拿到被註冊的方法。以 Input 爲例,在 Input 的 onChange 方法裏面去調用從父級 props 傳入的 formFieldChange 就能夠通知到 Form 組件了。

收集變更表單的數據。

前一步完成後,這一步就比較簡單了,Input 在調用 formFieldChange 的時候把想要傳遞的數據做爲參數傳進去,在 Form 裏面去對這個參數作處理,就能夠拿到當前變更的表單狀態數據了。

校驗表單是否填寫完成

前面咱們收集了每一條變更表單的數據。可是要判斷當前 Form 下面的表單是否填寫完成,那麼首先須要知道咱們有多少個須要填寫的表單,而後在 formFieldChange 的時候進行判斷就能夠了。如何來提早知道咱們有多少須要填寫的 Field 呢,以前我選擇的是經過在使用 Form 的時候先初始化一個包含全部表單初始化狀態的數據。

export default class Form extends React.Component {
  constructor(props) {
    super(props)
    this.Forms = []
    this.formState = Object.assign({}, {
      isComplete: false,
      isValidate: false,
      errorMsg: '',
      data: {}
    }, this.props.formState)
  }

  static propTypes = {
    onChange: PropTypes.func,
    onSubmit: PropTypes.func,
    formState: PropTypes.object
  }
 // 初始化一個相似這樣的對象傳遞給 Form
formState: {
  data: {
    realName: {},
    cityId: {},
    email: {},
    relativeName: {},
    relativePhone: {},
    companyName: {}
  }
},

這樣就很粗暴的解決了這個問題,可是這中間存在不少問題。

由於限定了特定的組件類型(Input,Select,CheckBox),致使不利於擴展,若是在開發過程遇到其餘類型的好比自定義的子表單,那麼 Form 就無法對這個自定義子表單進行數據收集,解決起來比較麻煩。

因此就在考慮另外一個種實現方式, Form 只去收集一個特定條件下的組件,只要這個組件知足了這個條件,並實現了對應的接口,那麼 Form 就均可以去收集處理。這樣也就大大挺高了適用性。

暴露對外提供整個表單狀態的方法

經過在外監聽每次 Form 觸發的 onChange 事件來獲取整個 Form 的狀態。

提交方法

檢驗表單是否經過校驗

已經有了整個 Form 的數據對象,作校驗並非什麼困難。經過校驗的時候調用 formSubmit 方法,沒有經過校驗的時候對外把錯誤信息添加到 Form 的 state 上去。

對外觸發 formSubmit 方法

當表單經過校驗的時候,對外觸發 formSubmit 方法,把要提交的數據做爲 formSubmit 的參數傳遞給外面。

After

前面是以前寫的 Form 組件的一些思路,在實際使用中也基本能知足業務需求。

可是整個 Form 的可拓展性比較差,沒法很好的接入其餘自定義的組件。因此萌生了重寫的想法。

對於重寫的這個 Form,個人想法是:首先必定要方便使用,不須要一大堆的起始工做;其次就是可拓展性要強,除了本身已經提供的內在 Input,Select 等可以接入 Form 外,對於其餘的業務中的特殊需求須要接入 Form 的時候,只要這個組件實現了特定的接口就能夠了很方便的接入,而不須要大量的去修改組件內部的代碼。

重構主要集中在上面需求 1 裏面的內容,也就是:__獲取當前變更表單的狀態__

獲取當前表單的狀態分解下來有一下幾點:

  • 獲取全部須要收集的子表單 formFields
  • 初始化 Form state
  • 表單下面子表單數量或類型發生變化時更新 1 裏面建立的 formFields
  • 子表單內部狀態發生變化時通知到父表單

獲取當前變更表單的狀態

獲取全部須要的子表單

一樣經過遞歸遍歷 children 來獲取須要收集的子表單,經過子表單的 type.name 命名規則是否符合咱們的定義來決定是否要進行收集。
直接來看代碼:

collectFormField = (children) => {

  const handleFieldChange = this.handleFieldChange

  // 簡單粗暴,在 Form 更新的時候直接清空上一次保存的 formFields,全量更新,
  // 避免 formFields 內容或者數量發生變化時 this.formFields 數據不正確的問題
  const FormFields = this.formFields = []

  function getChildList(children) {
    return React.Children.map(children, (el, i) => {
      // 只要 Name 以 _Field 開頭,就認爲是須要 From 管理的組件
      if (!el || el === null) return null

      const reg = /^_Field/
      const childName = el.type && el.type.name
      if (reg.test(childName)) {
        FormFields.push(el)
        return React.cloneElement(el, {
          key: i,
          handleFieldChange
        })
      } else {
        if (el.props && el.props.children) {
          const children = getChildList(el.props.children)
          return React.cloneElement(el, {
            key: i,
            children
          })
        } else {
          return el
        }
      }
    })
  }

只要組件的 class name 以 _Field 開頭,就把它收集起來,並傳入 handleFieldChange 方法,這樣當一個自定義組件接入的時候,只須要在外面包一層,並把 class 的命名爲以 _Field 開頭的格式就能夠被 Form 收集管理了。

接入組件裏面須要作的就是,在合適的時機調用 handleFieldChange 方法,並把要傳遞的數據做爲參數傳遞出來就能夠了。

爲何必定要執迷不悟的使用遍歷這種低效的方式去收集呢,其實都是爲了組件上使用的方便。這樣就不須要每次在引用的時候在對子表單作什麼操做了。

初始化 Form state

上一步拿到了全部的子表單,而後經過調用 initialFormDataStructure 拿來初始化 Form 的 state.data 的結構,同時通知到外面 Form 發生了變化。

子表單數量或類型發生變化時

當 Form 下面子組件被添加或刪除時,須要及時更新 Form Data 的結構。經過調用 updateFormDataStructure
把新增的或者修改的子表單更新到最新,並通知到外面 Form 發生了變化。

子表單內部狀態發生變化時

在第一步收集子表單的時候就已經把 handleFieldChange 注入到了子表單組件裏面,因此子表單來決定調用的時機。當 handleFieldChange 被調用的時候,首先對 Form state 進行更新,而後外通知子表單發生了變化,同時通知外面 Form 發生了變化。

這樣看起來整個流程就走通了,但實際上存在不少問題。

首先因爲 setState 是一個異步的過程,只有在 render 後才能獲取到最新的 state. 這就致使,在一個生命週期循環內若是我屢次調用了 setState ,那麼兩次調用之間對 state 的讀取極可能是不許確的。(有關生命週期的詳細內容能夠看這篇文章:https://www.w3ctech.com/topic...

因此我建立了一個臨時變量 currentState 來存放當前狀態下最新的 state,每次 setState 的時候都對其進行更新。

另外一個問題是當 Form 發生變化的時候,updateFormDataStructure 調用的過於頻繁。其實只有在子表單的數量或者類型發生變化時才須要更新 Form state 的結構。而直接去對比子表單的類型是否發生變化也是意見開銷很大操做,因此選擇另外一種折中方式。經過給 Form 當前的狀態打標,將 Form 可能處於的狀態都標識出來:

const STATUS = {
  Init: 'Init',
  Normal: 'Normal',
  FieldChange: 'FieldChange',
  UpdateFormDataStructure: 'UpdateFormDataStructure',
  Submit: 'Submit'
}

這樣,只有在 Form 的 STATUS 處於 Normal 的時候纔對其進行 updateFormDataStructure 操做。這樣就能夠省去不少次渲染以及無效的對外觸發的 FormChange 事件。

提交和對外暴露 Form 狀態的方法和以前基本一致,這樣整個對 Form 的重構就算完成了,具體項目中使用體驗還不錯 O(∩_∩)O

Form 組件地址: https://github.com/NE-LOAN-FED/NE-Component/tree/master/src/Form

最後,若是看文章的你有什麼更好的想法,請告訴我?。

相關文章
相關標籤/搜索