本文發佈於 個人博客
最近對團隊內部 React 組件庫(ne-rc)中的 Form 組件進行了重構,記錄一下思考的過程。react
一些前置定義:git
名詞 | 定義 |
---|---|
表單 | Form 組件 |
子表單 | 嵌套在 Form 下面的相似 Input, Select 這樣的子組件 |
首先咱們看一下,咱們的對 Form 組件的需求是什麼。github
獲取當前變更表單的狀態異步
formFieldChange
暴露對外提供整個表單狀態的方法this
提交方法code
formSubmit
方法接着咱們從重構前和重構後,看如何來解決這個問題。orm
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
的參數傳遞給外面。
前面是以前寫的 Form 組件的一些思路,在實際使用中也基本能知足業務需求。
可是整個 Form 的可拓展性比較差,沒法很好的接入其餘自定義的組件。因此萌生了重寫的想法。
對於重寫的這個 Form,個人想法是:首先必定要方便使用,不須要一大堆的起始工做;其次就是可拓展性要強,除了本身已經提供的內在 Input,Select 等可以接入 Form 外,對於其餘的業務中的特殊需求須要接入 Form 的時候,只要這個組件實現了特定的接口就能夠了很方便的接入,而不須要大量的去修改組件內部的代碼。
重構主要集中在上面需求 1 裏面的內容,也就是:__獲取當前變更表單的狀態__
獲取當前表單的狀態分解下來有一下幾點:
formFields
state
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
方法,並把要傳遞的數據做爲參數傳遞出來就能夠了。
爲何必定要執迷不悟的使用遍歷這種低效的方式去收集呢,其實都是爲了組件上使用的方便。這樣就不須要每次在引用的時候在對子表單作什麼操做了。
上一步拿到了全部的子表單,而後經過調用 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
最後,若是看文章的你有什麼更好的想法,請告訴我?。