基於React實現高度簡潔的Form表單方案

最近項目裏在作咱們本身的組件庫,關於表單這塊,如何實現一個更簡單的表單方案,是咱們一直在討論的問題,以前項目裏習慣用 ant-design 的 Form 表單,也以爲蠻好用的,咱們但願能作出更簡潔的方案。html

下面列出了表單相關的解決方案,React 社區的輪子真是多到沒法想象:react

以上的表單方案主要聚焦在一下幾點:git

  1. 更方便地作數據收集,不手寫 valueonChange,有的表單是增長函數(ant-design)或容器(FormBinderFusion等),爲子組件註冊 valueonChange,有的是自定義 Feild 組件(UForm),內部處理相關邏輯
  2. 更高效的渲染,好比字段狀態分佈式管理
  3. 簡單,下降學習成本
  4. 動態表單渲染

關於數據收集與渲染

關於表單數據收集,能夠參考雙向數據綁定,下面是雙向數據綁定的討論:github

以及關於實現雙向數據綁定的文章:算法

一個是數據收集,一個是渲染,也就是所謂的雙向數據綁定,總結起來有三個途徑:redux

  1. 能夠在編譯期進行代碼轉換,注入賦值語句和組件數據監聽方法,這個看起來高大上,須要本身寫 Babel 插件
  2. 運行時修改虛擬DOM,好比 ant-designice 等等,確實也都蠻好用的,上面列出的文章均可以研讀一下,頗有意義
  3. 手寫 valueonChange,除非你的系統裏只有一個表單。。。

先立個目標

看了大佬們的實現,咱們也想造個輪子,但願還能夠更簡潔,讓表單寫起來更開心,當系統裏有不少表單,都要手綁 valueonChange 確定是不行的,即使 ant-designice 等,還要加額外的函數或容器,因此目標就是下面這樣:緩存

import {Form,Input} form 'form';

export default class FormDemo extends Component<any, any> {
    public state = {
        value: {
            name: '',
            school: '',
        },
    }
    public onFormChange = (value) => {
        console.log(value);
        this.setState({
            value,
        });
    }
    public onFormSubmit = () => {
        // console.log('submit')
    }
    public render() {
        const me = this;
        const {
            value,
        } = me.state;
        return (
            <Form
                value={value}
                enableDomCache={false}
                onChange={me.onFormChange}
                onSubmit={me.onFormSubmit}
            >
                <div className="container">
                    <input
                        className="biz-input"
                        data-name="name"
                        data-rules={[{ max: 10, message: '最大長度10' }]}
                        type="text"
                    />
                    <Input
                        data-name="school"
                        data-rules={[{ max: 10, message: '最大長度10' }]}
                        type="text"
                    />
                    <Button type="primary" htmlType="submit">提交</Button>
                </div>
            </Form>
        )
    }
}


複製代碼
  1. 簡潔,貼近原生,學習成本低
  2. 組件兼容全部實現 valueonChange 的組件,好比 ant-design 的表單組件
  3. 表單驗證,沿用 ant-design 設計,使用 async-validator 庫來作

看得出來,咱們是 ant-design 的粉絲了,坦白說,大佬們的方案已經足夠簡潔了,ant-design 是先驅,後繼者 Ice , Fusion 等多對標 ant-design ,力圖更給出更簡潔的方案,他們也確實很簡潔,特別是 FusionField 組件,眼前一亮的感受,UForm 使用相似 JSON Schema(JSchema) 的語法寫表單,Uformfinal-form 強調字段的分佈式管理,高性能,不過,這兩個方案有必定的學習成本,實現方案天然是複雜的。性能優化

不過,當我說出咱們的實現,你們估計要吐槽,由於咱們的實現太簡單(捂臉),簡單到懷疑人生。bash

實現

要想實現上面的目標,顯然文章開頭文章列表已經有人實踐了,編譯期注入代碼,不過你要新加個 Babel 插件,不知道你喜不喜歡。async

咱們的實現是採用運行時修改虛擬DOM的,不在編譯期作,也就是運行時來作了,不過,不會在組件外加額外的函數或容器,只是利用 Form 容器來實現,你們必定想到了,那樣是否是要遍歷全部子節點?這樣會不會有額外的性能開銷?

那就先實現,再優化。

首先,須要遍歷全部子 虛擬DOM 節點,深度優先,判斷節點是否有 data-name 或者 name 屬性,若是有,爲該組件附加 valueonChange 屬性,像 checkbox, radio, select 等組件,特殊處理。

綁定value和onChange核心代碼(有刪減)以下:

public bindEvent(value, childList) {
    const me = this;
    if (!childList || React.Children.count(childList) === 0) {
        return;
    }
    React.Children.forEach(childList, (child) => {
        if (!child.props) {
            return;
        }
        const { children, onChange } = child.props;
        const bind = child.props['data-name'];
        const rules = child.props['data-rules'];
        // 分析節點類型,獲取對應的屬性名是value,仍是checked等
        const valuePropName = me.getValuePropName(child);
        if (bind) {
            child.props[valuePropName] = value[bind];
            if (!onChange) {                
                child.props.onChange = me.onFieldChange.bind(me, bind, valuePropName);
            }
        }
        me.bindEvent(value, children);
    });
}
複製代碼

onFieldChange的代碼:

public onFieldChange(fieldName, valuePropName, e) {
    const me = this;
    const {
        onChange = () => null,
        onFieldChange = () => null,
    } = me.props;
    let value;
    if (e.target) {
        value = e.target[valuePropName];
    } else {
        value = e;
    }
    me.updateValue(fieldName, value, () => {
        onFieldChange(e);
        const allValues = me.state.formData.value;
        onChange(allValues);
    })
}
複製代碼

上面代碼即使實現了咱們的目標,不用手綁 valueonChange 了。

演示:

表單

接下來是實現表單驗證,表單驗證,仍是沿用了 ant-design 的實現,使用async-validator這個庫來作,配置方式和 ant-design 是同樣的。爲了顯示驗證的錯誤信息,加入了 FormItem 容器,使用方式也貼近 ant-design

FormItem 的實現使用 React 的 Context API,具體能夠查看實現源碼,由於不是本文重點,就不說了。

ant-design 同樣,只要是實現 valueonChange 接口的組件,均可以在這裏使用,不限於原生的 HTML 組件。

關於性能的疑慮

經過上面的代碼實現咱們想要的目標,不過,仍是有疑問的地方:這個每次渲染都深度遍歷子節點,會不會有性能問題?

答案是:影響微乎其微

經過測試,1000 之內的表單控件感覺不到差異。1000 個子組件對 React 來講,diff算法開銷也很大的。

不過,爲了提高性能,咱們仍是作了優化,加入了虛擬 DOM 緩存

假如咱們在首次渲染後,將建立的虛擬 DOM 緩存下來,第二次渲染就不須要須要從新建立了,也不須要深度遍歷節點添加 valueonChange 了,可是爲了更新 value,須要獲取具備 data-name 節點的引用,將組件以 data-name 值爲 key 放到對象裏,更新的時候經過 data-name 值獲取這個組件,直接更新這個組件的虛擬 DOM 屬性就能夠了,直接獲取 DOM 引用更新 DOM,這看起來很 JQuery 吧?

經過上面的優化,性能能提高一倍。

不過,若是表單內組件有動態顯示、隱藏的話,就不能用虛擬DOM緩存了,因此,咱們提供了一個屬性 enableDomCache ,它能夠是布爾值,也能夠是一個函數,參數是以前的表單值,由用戶對當前值和前值比較,來肯定下次渲染是否使用緩存。不過,只有遇到性能問題的時候能夠考慮用它,多數時候沒有性能問題,這個 enableDomCache 默認設置爲 false

示例:

import {Form} form 'form';

export default class FormDemo extends Component<any, any> {
    state = {
        value: {
            name: '',
            school: '',
        },
    }
    onFormChange = (value) => {
        this.setState({
            value,
        });
    }
    onFormSubmit = () => {
        // console.log('submit')
    }
    enableDomCache=(preValue)=>{
        const me=this;
        const {
            value,
        } = me.state;

        if(preValue.showSchool!==value.showSchool){
            return false;
        }

        return true;
    }
    render(){
        const me=this;
        const {
            value,
        } = me.state;
        return (
            <Form 
                value={value}
                enableDomCache={me.enableDomCache}
                onChange={me.onFormChange} 
                onSubmit={me.onFormSubmit}
            >
                <input 
                    data-name={`name`} 
                    data-rules={[ { max: 3, message: '最大長度3', } ]} 
                    type="text" 
                />
                {
                    value.showSchool&&(
                        <input 
                            data-name={`school`} 
                            data-rules={[ { max: 3, message: '最大長度3', } ]} 
                            type="text" 
                        />
                    )
                }
            </Form>
        )
    }
}

複製代碼

關於字段的分佈式管理思考

若是每次表單的字段修改,都會致使整個表單從新渲染,確實不夠完美,因此會有字段分佈式管理的想法。

能夠考慮給表單加個 reduxstore ,每一個表單項組件訂閱 store,維護本身的數據狀態,表單項之間互不影響,這樣表單字段就是分佈式的了,store 存儲了最新的表單數據。

不過,大多數時候,即便從新渲染,用戶也體會不到其中的差異,ant-design 就是從新渲染,這裏說的從新渲染,是從新 render 建立虛擬 DOM,其實 React 進行 diff 後,真是的DOM並未所有渲染。

固然,爲了追求完美,避免 React 進行 diff,那就是最好了,因此對於表單內的重型組件,考慮利用 shouldComponentUpdate 進行更新控制,用過 Redux 同窗都知道,connect 高階組件內部是作了屬性的對比來控制組件是否更新的。

還有一點,受控組件和非受控組件的影響,若是表單自己是受控組件,那麼它的屬性改變,確定致使自己的從新渲染計算,因此要想更好的性能,最好是使用非受控組件模式,這個仍是要看具體須要,由於目前多數時候,狀態都會選擇全局狀態,非受控組件不會由於外部狀態改變而更新,因此可能會有UI狀態和全局狀態不一致的可能,若是表單數據的修改只有表單自己來控制,那就能夠放心使用非受控模式了。

補充,不管是受控和非受控,均可以利用 shouldComponentUpdate 進行組件自己的優化。

關於表單嵌套

在以前的文章討論中,看到用戶對錶單嵌套的需求,這個想起來不難,只要表單自己符合 value onChange 接口,那麼表單也能夠嵌套表單了,就像下面這樣:

import {Form,Input} form 'form';

export default class FormDemo extends Component {
    render(){
        const me=this;
        const {
            value,
        } = me.state;
        return (
            <Form value={value} onChange={me.onFormChange} onSubmit={me.onFormSubmit} >
                <input  data-name="name" type="text" />
                <Input  data-name="school" type="text" />
                <Form name="children1">
                    <input  data-name="name" type="text" />
                    <Input  data-name="school" type="text" />
                    <Form name="children2">
                        <input  data-name="name" type="text" />
                        <Input  data-name="school" type="text" />
                        <Form name="children3">
                            <input  data-name="name" type="text" />
                            <Input  data-name="school" type="text" />
                        </Form>
                         <Form name="children4">
                            <input  data-name="name" type="text" />
                            <Input  data-name="school" type="text" />
                        </Form>
                    </Form>
                </Form>
            </Form>
        )
    }
}
複製代碼

演示:

嵌套表單

雖然實現了表單嵌套,可是這個實現是有問題的,子表單的數據變動,會沿着 onChange 方法逐級向上傳遞,當數據量大,嵌套層級深的時候,會有性能問題。

嵌套表單數據變動演示:

嵌套表單

最好相似於字段的分佈式管理同樣,每一個表單只負責本身的渲染,不會致使其餘表單從新渲染,爲了提高性能,咱們進行了優化,提供了 FormGroup 容器,這個容器能夠遍歷 Form 節點,構建 Form 節點的引用關係,爲每一個 Form 生成一個惟一 ID,將全部 Form 的狀態統一由 FormGroup 的 state 管理,至關於進行了扁平化,而不是像原來同樣,子級 FormValue 由父級的來管理。

狀態偏平化後,每一個表單的變動只會致使自身從新渲染,不影響其餘表單。

演示:

嵌套表單

可是,上面的優化僅限於非受控狀態下,由於受控狀態下,仍是要由外部屬性傳入 valueFormGroup,而內部 value 的和屬性傳入的 value 結構不一致,一個是扁平的結構,一個樹形結構,由樹形結構轉扁平結構的條件不充分,由於不知道表單的嵌套結構,因此 value 的轉換作不到了。

總之,簡單的樹形結構能夠不使用 FormGroup 。複雜的能夠考慮使用 FormGroup ,而且設置 defaultValue 而不是 value,來使用非受控的模式。

最後

本文嘗試構建了一個更簡潔的表單方案,利用深度遍歷子節點的方法爲子組件賦值 value 以及註冊 onChange 事件,表單的書寫上更加貼近原生,更加簡潔,也利用緩存虛擬DOM的方法對深度遍歷子節點這種方式進行了性能優化,嘗試實現表單嵌套,而且利用 FormGroup 容器進行數據更新扁平化,不知道你有沒有收穫。

最後的最後

這看起來很像Vue是吧?,React不像Vue有那麼多指令能夠輔助,因此表單這塊會有那麼多的方案來簡化,不過想起來,上面的作法和ast的解析執行很相似,雖然不能編譯期作,可是運行期作也能夠,那麼會不會出現一個Template組件,來提供魔法指令?

而後寫出下面的代碼:

<Template>
    <div v-if={true}>
        {name}
    </div>
    <div v-show={true}>
        <div/>
    </div>
</Template>

複製代碼

文章僅供參考,提供解決問題的思路,歡迎你們評論,謝謝!

相關文章
相關標籤/搜索