摘要:本文發(fā)布于我的博客最近對(duì)團(tuán)隊(duì)內(nèi)部組件庫(kù)中的組件進(jìn)行了重構(gòu),記錄一下思考的過(guò)程。暴露對(duì)外提供整個(gè)表單狀態(tài)的方法通過(guò)在外監(jiān)聽(tīng)每次觸發(fā)的事件來(lái)獲取整個(gè)的狀態(tài)。子表單數(shù)量或類型發(fā)生變化時(shí)當(dāng)下面子組件被添加或刪除時(shí),需要及時(shí)更新的結(jié)構(gòu)。
本文發(fā)布于 我的博客
最近對(duì)團(tuán)隊(duì)內(nèi)部 React 組件庫(kù)(ne-rc)中的 Form 組件進(jìn)行了重構(gòu),記錄一下思考的過(guò)程。
一些前置定義:
名詞 | 定義 |
---|---|
表單 | Form 組件 |
子表單 | 嵌套在 Form 下面的類似 Input, Select 這樣的子組件 |
首先我們看一下,我們的對(duì) Form 組件的需求是什么。
獲取當(dāng)前變動(dòng)表單的狀態(tài)
校驗(yàn)所有必填表單是否填寫(xiě)完成
對(duì)外觸發(fā)具體表單變化的方法 formFieldChange
暴露對(duì)外提供整個(gè)表單狀態(tài)的方法
提供整個(gè)表單最新?tīng)顟B(tài)的方法 $Form.data
提交方法
校驗(yàn)表單是否通過(guò)校驗(yàn)
對(duì)外觸發(fā) formSubmit 方法
接著我們從重構(gòu)前和重構(gòu)后,看如何來(lái)解決這個(gè)問(wèn)題。
Before 獲取當(dāng)前變動(dòng)表單的狀態(tài) 如何獲取變動(dòng)的子表單React 父子通信需要通過(guò) prop 傳遞方法,對(duì)于 Form 下面的類似與 Input 之類的子表單的變化想要通知到父級(jí),如果不借助第三方的事件傳遞方法,那么就只能通過(guò)由父級(jí)通過(guò) props 向 Input 傳遞 formFieldChange(假設(shè)就叫這個(gè)名字)方法,然后當(dāng)子組件變化時(shí)去調(diào)用 formFieldChange 來(lái)實(shí)現(xiàn)。
那么問(wèn)題來(lái)了,什么時(shí)候去傳遞這個(gè)方法呢?
不能在具體頁(yè)面里面使用的時(shí)候再去每條表單里面注冊(cè)這個(gè)方法,那每個(gè)用到表單組件的時(shí)候就都需要給子表單進(jìn)行這樣的事件綁定,這樣太累了。
所以一開(kāi)始,我選擇通過(guò)直接遞歸的遍歷 Form 下面的 children,只要發(fā)現(xiàn)這個(gè) children 是我想要的表單類型,那么就重新克隆一個(gè)帶有 formFieldChange 的組件來(lái)替換掉原來(lái)的組件。
/** * 獲取 form 下面每一個(gè)表單對(duì)象,注入屬性,并收集起來(lái) * @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 } } }) }
這樣,所有的特定子組件就都可以拿到被注冊(cè)的方法。以 Input 為例,在 Input 的 onChange 方法里面去調(diào)用從父級(jí) props 傳入的 formFieldChange 就可以通知到 Form 組件了。
收集變動(dòng)表單的數(shù)據(jù)。前一步完成后,這一步就比較簡(jiǎn)單了,Input 在調(diào)用 formFieldChange 的時(shí)候把想要傳遞的數(shù)據(jù)作為參數(shù)傳進(jìn)去,在 Form 里面去對(duì)這個(gè)參數(shù)做處理,就可以拿到當(dāng)前變動(dòng)的表單狀態(tài)數(shù)據(jù)了。
校驗(yàn)表單是否填寫(xiě)完成前面我們收集了每一條變動(dòng)表單的數(shù)據(jù)。但是要判斷當(dāng)前 Form 下面的表單是否填寫(xiě)完成,那么首先需要知道我們有多少個(gè)需要填寫(xiě)的表單,然后在 formFieldChange 的時(shí)候進(jìn)行判斷就可以了。如何來(lái)提前知道我們有多少需要填寫(xiě)的 Field 呢,之前我選擇的是通過(guò)在使用 Form 的時(shí)候先初始化一個(gè)包含所有表單初始化狀態(tài)的數(shù)據(jù)。
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 } ?// 初始化一個(gè)類似這樣的對(duì)象傳遞給 Form formState: { data: { realName: {}, cityId: {}, email: {}, relativeName: {}, relativePhone: {}, companyName: {} } },
這樣就很粗暴的解決了這個(gè)問(wèn)題,但是這中間存在很多問(wèn)題。
因?yàn)橄薅颂囟ǖ慕M件類型(Input,Select,CheckBox),導(dǎo)致不利于擴(kuò)展,如果在開(kāi)發(fā)過(guò)程遇到其他類型的比如自定義的子表單,那么 Form 就沒(méi)法對(duì)這個(gè)自定義子表單進(jìn)行數(shù)據(jù)收集,解決起來(lái)比較麻煩。
所以就在考慮另一個(gè)種實(shí)現(xiàn)方式, Form 只去收集一個(gè)特定條件下的組件,只要這個(gè)組件滿足了這個(gè)條件,并實(shí)現(xiàn)了對(duì)應(yīng)的接口,那么 Form 就都可以去收集處理。這樣也就大大挺高了適用性。
暴露對(duì)外提供整個(gè)表單狀態(tài)的方法通過(guò)在外監(jiān)聽(tīng)每次 Form 觸發(fā)的 onChange 事件來(lái)獲取整個(gè) Form 的狀態(tài)。
提交方法 檢驗(yàn)表單是否通過(guò)校驗(yàn)已經(jīng)有了整個(gè) Form 的數(shù)據(jù)對(duì)象,做校驗(yàn)并不是什么困難。通過(guò)校驗(yàn)的時(shí)候調(diào)用 formSubmit 方法,沒(méi)有通過(guò)校驗(yàn)的時(shí)候?qū)ν獍彦e(cuò)誤信息添加到 Form 的 state 上去。
對(duì)外觸發(fā) formSubmit 方法當(dāng)表單通過(guò)校驗(yàn)的時(shí)候,對(duì)外觸發(fā) formSubmit 方法,把要提交的數(shù)據(jù)作為 formSubmit 的參數(shù)傳遞給外面。
After前面是之前寫(xiě)的 Form 組件的一些思路,在實(shí)際使用中也基本能滿足業(yè)務(wù)需求。
但是整個(gè) Form 的可拓展性比較差,無(wú)法很好的接入其他自定義的組件。所以萌生了重寫(xiě)的想法。
對(duì)于重寫(xiě)的這個(gè) Form,我的想法是:首先一定要方便使用,不需要一大堆的起始工作;其次就是可拓展性要強(qiáng),除了自己已經(jīng)提供的內(nèi)在 Input,Select 等能夠接入 Form 外,對(duì)于其他的業(yè)務(wù)中的特殊需求需要接入 Form 的時(shí)候,只要這個(gè)組件實(shí)現(xiàn)了特定的接口就可以了很方便的接入,而不需要大量的去修改組件內(nèi)部的代碼。
重構(gòu)主要集中在上面需求 1 里面的內(nèi)容,也就是:__獲取當(dāng)前變動(dòng)表單的狀態(tài)__
獲取當(dāng)前表單的狀態(tài)分解下來(lái)有一下幾點(diǎn):
獲取所有需要收集的子表單 formFields
初始化 Form state
表單下面子表單數(shù)量或類型發(fā)生變化時(shí)更新 1 里面創(chuàng)建的 formFields
子表單內(nèi)部狀態(tài)發(fā)生變化時(shí)通知到父表單
獲取當(dāng)前變動(dòng)表單的狀態(tài) 獲取所有需要的子表單同樣通過(guò)遞歸遍歷 children 來(lái)獲取需要收集的子表單,通過(guò)子表單的 type.name 命名規(guī)則是否符合我們的定義來(lái)決定是否要進(jìn)行收集。
直接來(lái)看代碼:
collectFormField = (children) => { const handleFieldChange = this.handleFieldChange // 簡(jiǎn)單粗暴,在 Form 更新的時(shí)候直接清空上一次保存的 formFields,全量更新, // 避免 formFields 內(nèi)容或者數(shù)量發(fā)生變化時(shí) this.formFields 數(shù)據(jù)不正確的問(wèn)題 const FormFields = this.formFields = [] function getChildList(children) { return React.Children.map(children, (el, i) => { // 只要 Name 以 _Field 開(kāi)頭,就認(rèn)為是需要 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 開(kāi)頭,就把它收集起來(lái),并傳入 handleFieldChange 方法,這樣當(dāng)一個(gè)自定義組件接入的時(shí)候,只需要在外面包一層,并把 class 的命名為以 _Field 開(kāi)頭的格式就可以被 Form 收集管理了。
接入組件里面需要做的就是,在合適的時(shí)機(jī)調(diào)用 handleFieldChange 方法,并把要傳遞的數(shù)據(jù)作為參數(shù)傳遞出來(lái)就可以了。
為什么一定要執(zhí)迷不悟的使用遍歷這種低效的方式去收集呢,其實(shí)都是為了組件上使用的方便。這樣就不需要每次在引用的時(shí)候在對(duì)子表單做什么操作了。
初始化 Form state上一步拿到了所有的子表單,然后通過(guò)調(diào)用 initialFormDataStructure 拿來(lái)初始化 Form 的 state.data 的結(jié)構(gòu),同時(shí)通知到外面 Form 發(fā)生了變化。
子表單數(shù)量或類型發(fā)生變化時(shí)當(dāng) Form 下面子組件被添加或刪除時(shí),需要及時(shí)更新 Form Data 的結(jié)構(gòu)。通過(guò)調(diào)用 updateFormDataStructure
把新增的或者修改的子表單更新到最新,并通知到外面 Form 發(fā)生了變化。
在第一步收集子表單的時(shí)候就已經(jīng)把 handleFieldChange 注入到了子表單組件里面,所以子表單來(lái)決定調(diào)用的時(shí)機(jī)。當(dāng) handleFieldChange 被調(diào)用的時(shí)候,首先對(duì) Form state 進(jìn)行更新,然后外通知子表單發(fā)生了變化,同時(shí)通知外面 Form 發(fā)生了變化。
這樣看起來(lái)整個(gè)流程就走通了,但實(shí)際上存在很多問(wèn)題。
首先由于 setState 是一個(gè)異步的過(guò)程,只有在 render 后才能獲取到最新的 state. 這就導(dǎo)致,在一個(gè)生命周期循環(huán)內(nèi)如果我多次調(diào)用了 setState ,那么兩次調(diào)用之間對(duì) state 的讀取很可能是不準(zhǔn)確的。(有關(guān)生命周期的詳細(xì)內(nèi)容可以看這篇文章:https://www.w3ctech.com/topic...)
所以我創(chuàng)建了一個(gè)臨時(shí)變量 currentState 來(lái)存放當(dāng)前狀態(tài)下最新的 state,每次 setState 的時(shí)候都對(duì)其進(jìn)行更新。
另一個(gè)問(wèn)題是當(dāng) Form 發(fā)生變化的時(shí)候,updateFormDataStructure 調(diào)用的過(guò)于頻繁。其實(shí)只有在子表單的數(shù)量或者類型發(fā)生變化時(shí)才需要更新 Form state 的結(jié)構(gòu)。而直接去對(duì)比子表單的類型是否發(fā)生變化也是意見(jiàn)開(kāi)銷很大操作,所以選擇另一種折中方式。通過(guò)給 Form 當(dāng)前的狀態(tài)打標(biāo),將 Form 可能處于的狀態(tài)都標(biāo)識(shí)出來(lái):
const?STATUS?=?{ ??Init:?"Init", ??Normal:?"Normal", ??FieldChange:?"FieldChange", ??UpdateFormDataStructure:?"UpdateFormDataStructure", ??Submit:?"Submit" }
這樣,只有在 Form 的 STATUS 處于 Normal 的時(shí)候才對(duì)其進(jìn)行 updateFormDataStructure 操作。這樣就可以省去很多次渲染以及無(wú)效的對(duì)外觸發(fā)的 FormChange 事件。
提交和對(duì)外暴露 Form 狀態(tài)的方法和之前基本一致,這樣整個(gè)對(duì) Form 的重構(gòu)就算完成了,具體項(xiàng)目中使用體驗(yàn)還不錯(cuò) O(∩_∩)O
Form 組件地址: https://github.com/NE-LOAN-FED/NE-Component/tree/master/src/Form
最后,如果看文章的你有什么更好的想法,請(qǐng)告訴我?。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/82085.html
摘要:背景目前是社區(qū)最炙手可熱的新技術(shù),我們準(zhǔn)備追一下熱度,在當(dāng)前的項(xiàng)目中實(shí)踐一下技術(shù)。我們的項(xiàng)目使用的腳手架是,初步想法是把現(xiàn)有的一個(gè)有狀態(tài)頁(yè)面組件重構(gòu)成函數(shù)組件。存放表單值的狀態(tài)是聲明在列表組件,傳給表單組件。 背景 React Hooks目前是React社區(qū)最炙手可熱的新技術(shù),我們準(zhǔn)備追一下熱度,在當(dāng)前的項(xiàng)目中實(shí)踐一下Hooks技術(shù)。 我們的項(xiàng)目使用的腳手架是Ant Design P...
摘要:等價(jià)于是一個(gè)返回函數(shù)的函數(shù)就是個(gè)高階函數(shù)返回的函數(shù)就是一個(gè)高階組件,該高階組件返回一個(gè)與關(guān)聯(lián)起來(lái)的新組件的也是一樣的總結(jié)一下高階組件是對(duì)代碼進(jìn)行更高層次重構(gòu)的好方法,如果你想精簡(jiǎn)你的和生命周期方法,那么高階組件可以幫助你提取出可重用的函數(shù)。 談到react,我們第一個(gè)想到的應(yīng)該是組件,在react的眼中可真的是萬(wàn)物皆組件。就連我們獲取數(shù)據(jù)用到的axios也可以用組件來(lái)表示...比如,我...
摘要:等價(jià)于是一個(gè)返回函數(shù)的函數(shù)就是個(gè)高階函數(shù)返回的函數(shù)就是一個(gè)高階組件,該高階組件返回一個(gè)與關(guān)聯(lián)起來(lái)的新組件的也是一樣的總結(jié)一下高階組件是對(duì)代碼進(jìn)行更高層次重構(gòu)的好方法,如果你想精簡(jiǎn)你的和生命周期方法,那么高階組件可以幫助你提取出可重用的函數(shù)。 談到react,我們第一個(gè)想到的應(yīng)該是組件,在react的眼中可真的是萬(wàn)物皆組件。就連我們獲取數(shù)據(jù)用到的axios也可以用組件來(lái)表示...比如,我...
摘要:等價(jià)于是一個(gè)返回函數(shù)的函數(shù)就是個(gè)高階函數(shù)返回的函數(shù)就是一個(gè)高階組件,該高階組件返回一個(gè)與關(guān)聯(lián)起來(lái)的新組件的也是一樣的總結(jié)一下高階組件是對(duì)代碼進(jìn)行更高層次重構(gòu)的好方法,如果你想精簡(jiǎn)你的和生命周期方法,那么高階組件可以幫助你提取出可重用的函數(shù)。 談到react,我們第一個(gè)想到的應(yīng)該是組件,在react的眼中可真的是萬(wàn)物皆組件。就連我們獲取數(shù)據(jù)用到的axios也可以用組件來(lái)表示...比如,我...
摘要:等價(jià)于是一個(gè)返回函數(shù)的函數(shù)就是個(gè)高階函數(shù)返回的函數(shù)就是一個(gè)高階組件,該高階組件返回一個(gè)與關(guān)聯(lián)起來(lái)的新組件的也是一樣的總結(jié)一下高階組件是對(duì)代碼進(jìn)行更高層次重構(gòu)的好方法,如果你想精簡(jiǎn)你的和生命周期方法,那么高階組件可以幫助你提取出可重用的函數(shù)。 談到react,我們第一個(gè)想到的應(yīng)該是組件,在react的眼中可真的是萬(wàn)物皆組件。就連我們獲取數(shù)據(jù)用到的axios也可以用組件來(lái)表示...比如,我...
閱讀 1559·2021-11-17 09:33
閱讀 1111·2021-11-12 10:36
閱讀 2422·2019-08-30 15:54
閱讀 2446·2019-08-30 13:14
閱讀 2920·2019-08-26 14:05
閱讀 3296·2019-08-26 11:32
閱讀 3011·2019-08-26 10:09
閱讀 3005·2019-08-26 10:09