最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 基于Antd 3/4的B端配置化表单解决方案

    正文概述 掘金(jssoscar)   2021-03-13   862

    背景

    B端中台项目开发中,表单的开发,是家常便饭的事儿。一般会涉及到大量的重复性工作:

    • 字段模板编写
    • 字段规则编写
    • 字段属性的定制与配置
    • 表单联动处理
    • 编辑(编辑、新增同时可以理解为编辑态)、查看态页面编写
    • 数据抹平适配处理

    前端编写表单同时,复杂场景下还需要配置一些结构下发前端来解析、服务端也需要编写字段校验规则。

    伴随业务变更、复杂化、场景动态化视图(现在负责的项目中,工单系统,常规下几百个表单项,复杂场景下1000+表单字段,没错1000+?)、前后端开发同步不及时的情况,前后端配置、校验规则可能会存在不一致的问题。

    因此,开发一套满足前后端均可配置的配置化表单方案,用来减少团队重复性工作,是必须执行的一个技术方案。

    开发现状

    现阶段,团队项目技术栈是React,主要使用Antd 3/ Antd 4来支持业务开发。

    整体上,表单常规开发不出下面几种代码片段:

    3版本方案原始代码

    常规布局

    基于Antd 3/4的B端配置化表单解决方案

    多列布局

    基于Antd 3/4的B端配置化表单解决方案

    4版本方案原始代码

    常规布局

    基于Antd 3/4的B端配置化表单解决方案

    多列布局

    基于Antd 3/4的B端配置化表单解决方案

    日常开发中,少量几个字段开发还可勉强code,出现几十、上百、上千后,整体代码大量得复制粘贴改一改,开发同学也从前端developer升级优秀的前端copier?。

    设计、开发实现一套优雅、扩展性强得配置化的表单方案,是需要解决得问题。

    目标

    整体上,从完整的方案角度来看,需要梳理下以下实现:

    常规实现

    • Antd基础类型的可配置化
    • Antd元素校验配置
    • 不同表单配置的属性处理(比如:valueProp不同字段的配置)
    • 不同表单元素类型展示UI出现错乱的问题
    • 元素布局方式

    增强方案

    • 数据:数据自动注入、数据类型抹平
    • 类型增强:.trim能力(借鉴vue中.trim语法)、text类型、html、hidden类型等增强
    • 表单级联
    • 自定义模板、自定义组件注册
    • 非表单元素的嵌入
    • 服务端化下发的配置的解析(其中主要是,校验规则中的正则的处理)

    个性化

    • 组件继承
    • 参数、属性全局配置
    • 布局、继承的全局配置
    • 状态:编辑态/查看态的适配
    • 接下来,奔着整体实现来设计我们的技术方案。

    方案设计

    Schema抽离

    从原始的Antd 3/4原始开发代码角度出发,进行抽离。

    1、对代码片段进行结构抽离:

    基于Antd 3/4的B端配置化表单解决方案

    2、模型形成

    基于Antd 3/4的B端配置化表单解决方案

    3、形成基础版本Schema

    Antd 3

    基于Antd 3/4的B端配置化表单解决方案

    Antd 4

    基于Antd 3/4的B端配置化表单解决方案

    常规实现

    1. 整体上,常规类型均已内置。现在情况下,提供近30种表单类型
    2. 对于单checkbox、switch、upload等配置时,抹平valuePropName配置
    3. 解决不同表单元素类型展示UI出现错乱的问题
    4. 基础布局可配置

    增强方案

    input/textarea组件的增强

    • 增加.trim语法糖,扩展antd中input/textarea缺少trim能力
    • input.trim/textarea.trim作为内置类型

    数据处理

    数据自动注入

    initialValue需要绑定每个字段,提供data配置,表单元素通过id自动绑注入initialValue

    数据抹平

    对于时间、日期类组件,antd需要moment类型。通过类型处理,自动转换数据为组件所需类型,开发者无需感知

    表单级联

    • 基础结构上,增加logic层,完成级联实现。级联的实现,不仅简单的展示、隐藏这种,可以做到元素的全更新(比如:类型、展示、事件绑定、校验规则等等)
    • 根据开发者配置的test规则,匹配后,自动合并元素配置,完成级联逻辑的生效
    • 支持string/[]/object类型多种场景配置

    自定义组件注册

    • 提供register方法,支持用户自定义组件的注册
    • 3版本中使用hooks组件: 支持hooks类型组件的注册

    自定义模板支持

    支持自定义模板嵌入 支持非表单元素嵌入

    个性化

    • 组件继承:提供extends配置,扩展表单元素
    • 增加setGlobalConfig,进行属性、参数、继承等全局配置
    • 表单状态:增加status字段,标记视图编辑、查看态,完成1份json两份视图的展示

    整体方案完成后,单元素配置如下图:

    基于Antd 3/4的B端配置化表单解决方案

    工作原理

    项目运行流程

    基于Antd 3/4的B端配置化表单解决方案

    整体使用情况

    当前方案在所属团队,经过2年多沉淀与打磨,已接入20+工作台,完成表单、视图的快速开发。

    项目遇到问题

    问题

    开发者项目使用了babel-import插件来进行antd的异步加载,使用item-generator后,系统样式丢失。

    原因

    babel-import对antd跟踪第一层引用,不会再额外处理node_modules中组件的依赖。

    解决方案

    提供item-generator/lib/Style模块,对于按需引入antd的项目,提供依赖组件的样式供开发者引入。

    后期规划

    2021,整体的一个大方向规划是:可视化。

    通过可视化方式拖拽、配置,实现可嵌套的栅格布局界面,完成开发、需求方的快速页面配置。

    完整代码片段(以3版本为例)

    ` import React, { PureComponent } from 'react'; import { Form, Button, Row } from 'antd'; import ItemGenerator, { setGlobalConfig } from 'item-generator'; import City from './City';

    // 设置全局配置 setGlobalConfig({ params: { showPleaseSel: false, // 不显示select的【请选择】选项 label: 'value', // 所有配置类数据的展示文本 value: 'id' // 所有配置类数据的值 }, colProps: { span: 12 // 全局表单布局,全局为2列布局 }, extends: { inputRequired: { item: { options: { rules: [ { required: true, message: '请输入' } ] } } }, selectRequired: { item: { options: { rules: [ { required: true, message: '请选择' } ] } } } } });

    // 注册自定义组件 register('city', City);

    // 注册hooks组件 register('hooks', Hooks, true);

    class Test extends PureComponent { state = { status: 1, colable: true };

    btnClicked = (status) => {
        this.setState({
            status
        });
    };
    
    resetForm = () => {
        const { form } = this.props;
        form.resetFields();
    };
    
    config = [
        {
            id: 1,
            value: '未成年人',
            children: [
                {
                    id: 10,
                    value: '0-10岁'
                }
            ]
        },
        {
            id: 2,
            value: '成年人',
            children: [
                {
                    id: 20,
                    value: '16-60岁'
                },
                {
                    id: 21,
                    value: '60岁以上'
                }
            ]
        },
        {
            id: 3,
            value: '未知'
        }
    ];
    
    query = () => {
        const { form } = this.props;
        console.log('表单数据:', form.getFieldsValue());
    };
    
    render() {
        const { form } = this.props;
        const { status, colable } = this.state;
        const { config } = this;
        const options = {
            config: [
                {
                    item: {
                        id: 'id',
                        label: 'ID',
                        type: 'hidden'
                    }
                },
                {
                    item: {
                        id: 'name',
                        label: 'input基础(级联)'
                    },
                    logic: 'nameNotRequired',
                    extends: 'inputRequired'
                },
                {
                    item: {
                        id: 'inputtrim',
                        label: 'input去空格(级联)'
                    },
                    logic: {
                        test: '{age} == 1',
                        item: {
                            options: {
                                rules: [
                                    {
                                        required: true
                                    }
                                ]
                            }
                        }
                    }
                },
                {
                    item: {
                        id: 'number',
                        label: '数字(级联)',
                        type: 'number'
                    },
                    logic: [
                        {
                            test: '{age} == 1',
                            show: true
                        },
                        {
                            test: '{ageMulit}.includes(1)',
                            item: {
                                options: {
                                    rules: [
                                        {
                                            required: true
                                        }
                                    ]
                                }
                            }
                        }
                    ]
                },
                {
                    item: {
                        id: 'age',
                        type: 'select',
                        label: '基础Select(级联)',
                        data: config,
                        params: {
                            shouldOptionDisabled: (val) => val == 2,
                            showTooltip: true,
                            tooltip: 'value',
                            tooltipProps: {
                                placement: 'right'
                            },
                            showPleaseSel: true,
                            pleaseSelValue: -1
                        }
                    },
                    extends: 'selectRequired'
                },
                {
                    item: {
                        id: 'ageMulit',
                        type: 'select',
                        label: status ? 'Select多选必填' : 'Select多选必填独占一行',
                        data: config,
                        props: {
                            mode: 'multiple'
                        }
                    },
                    extends: 'selectRequired'
                },
                {
                    item: {
                        id: 'treeselect',
                        label: '树形Select',
                        type: 'treeselect',
                        data: config,
                        params: {
                            shouldOptionDisabled: (val) => val == 2
                        }
                    }
                },
                {
                    item: {
                        id: 'ageGroup',
                        type: 'select',
                        label: 'Select分组',
                        data: config,
                        params: {
                            optGroup: true
                        }
                    }
                },
                {
                    item: {
                        id: 'cascader',
                        type: 'cascader',
                        label: '级联选择',
                        data: config,
                        params: {
                            shouldOptionDisabled: (val) => val == 1
                        }
                    }
                },
                {
                    item: {
                        id: 'checkbox',
                        label: '复选框',
                        type: 'checkbox'
                    }
                },
                {
                    item: {
                        id: 'checkboxgroup',
                        label: '多选框',
                        type: 'checkboxgroup',
                        data: config,
                        params: {
                            shouldOptionDisabled: (val) => val == 1
                        }
                    }
                },
                {
                    item: {
                        id: 'radio',
                        label: '单选框',
                        type: 'radio'
                    }
                },
                {
                    item: {
                        id: 'radiogroup',
                        label: '单选框组合',
                        type: 'radiogroup',
                        params: {
                            shouldOptionDisabled: (val) => val == 1
                        },
                        data: config
                    }
                },
                {
                    item: {
                        id: 'radiogroupbutton',
                        label: '多单选按钮框',
                        type: 'radiogroupbutton',
                        params: {
                            shouldOptionDisabled: (val) => val == 1
                        },
                        data: config
                    }
                },
                {
                    item: {
                        id: 'datepicker',
                        type: 'datepicker',
                        label: '日期'
                    }
                },
                {
                    item: {
                        id: 'rangepicker',
                        type: 'rangepicker',
                        label: '区间'
                    }
                },
                {
                    item: {
                        id: 'weekpicker',
                        type: 'weekpicker',
                        label: '周'
                    }
                },
                {
                    item: {
                        id: 'monthpicker',
                        type: 'monthpicker',
                        label: '月份'
                    }
                },
                {
                    item: {
                        id: 'timepicker',
                        type: 'timepicker',
                        label: '时间'
                    }
                },
                {
                    item: {
                        id: 'switch',
                        label: 'Switch开关',
                        type: 'switch'
                    }
                },
                {
                    colProps: {
                        style: {
                            height: 64
                        }
                    },
                    item: {
                        id: 'slider',
                        label: '滑动输入条',
                        type: 'slider'
                    }
                },
                {
                    item: {
                        label: 'html类型',
                        type: 'html',
                        template:
                            '<div style="font-size: 14px;color: red"><p>我是DangerHtml测试</p></div>'
                    }
                },
                {
                    item: {
                        label: '非表单元素',
                        template: <div>我是非表单元素展示到表单中</div>,
                        formable: false
                    }
                },
                {
                    item: {
                        id: 'search1',
                        label: '搜索提示',
                        type: 'suggest',
                        params: {
                            label: 'name',
                            onSearch: (name) =>
                                get('/formsearch/users', { name }).then(({ data }) => data)
                        }
                    }
                },
                {
                    item: {
                        id: 'search2',
                        label: '搜索提示多选',
                        type: 'suggest',
                        props: {
                            mode: 'multiple'
                        },
                        params: {
                            label: 'region',
                            onSearch: (city) =>
                                get('/formsearch/citys', {
                                    city
                                }).then(({ data }) => data)
                        }
                    }
                },
                {
                    item: {
                        id: 'textarea',
                        label: '文本框',
                        type: 'textarea'
                    }
                },
                {
                    item: {
                        id: 'hooks',
                        label: '自定义hooks组件',
                        type: 'hooks'
                    }
                },
                {
                    formItemProps: {
                        wrapperCol: {
                            lg: 20
                        },
                        labelCol: {
                            lg: 4
                        }
                    },
                    colProps: {
                        span: 24
                    },
                    item: {
                        id: 'textareatrim',
                        label: '文本框trim',
                        type: 'textarea.trim'
                    }
                },
                {
                    colProps: {
                        span: 24
                    },
                    formItemProps: {
                        labelCol: {
                            sm: 4
                        },
                        wrapperCol: {
                            sm: 20
                        }
                    },
                    item: {
                        label: '自定义注册组件',
                        type: 'city',
                        formable: false
                    }
                }
            ],
            status,
            data: {
                name: '测试账号',
                age: 1,
                ageMulit: [],
                id: 2,
                cascader: [2, 20]
            },
            colable,
            colProps: {
                span: 12
            },
            logic: {
                nameNotRequired: [
                    {
                        test: '{age} == 1',
                        item: {
                            options: {
                                rules: [
                                    {
                                        required: false,
                                        message: '请输入'
                                    }
                                ]
                            }
                        }
                    }
                ]
            }
        };
    
        const getBtn = (text, props) => (
            <Button
                type="primary"
                style={{
                    marginRight: 10
                }}
                {...props}
            >
                {text}
            </Button>
        );
        return (
            <Form
                autoComplete="off"
                labelCol={{
                    span: 6
                }}
                wrapperCol={{
                    span: 18
                }}
                style={{
                    padding: '20px 40px'
                }}
            >
                <Row gutter={4}>
                    <ItemGenerator form={form} options={options} />
                </Row>
                <div
                    style={{
                        display: 'flex',
                        justifyContent: 'flex-end'
                    }}
                >
                    {getBtn('查看状态', {
                        onClick: () => this.btnClicked(0)
                    })}
                    {getBtn('编辑状态', {
                        onClick: () => this.btnClicked(1)
                    })}
                    {getBtn('查询', {
                        onClick: this.query
                    })}
                    {getBtn('重置', {
                        onClick: this.resetForm
                    })}
                    {getBtn('切换布局', {
                        onClick: () =>
                            this.setState({
                                colable: !colable
                            })
                    })}
                </div>
            </Form>
        );
    }
    

    }

    export default Form.create()(Test);`


    起源地下载网 » 基于Antd 3/4的B端配置化表单解决方案

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元