基于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>

复制代码

文章仅供参考,提供解决问题的思路,欢迎你们评论,谢谢!

相关文章
相关标签/搜索