问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

react动态表单的思路和实现

发布网友 发布时间:2024-09-27 14:50

我来回答

1个回答

热心网友 时间:2024-10-25 03:21

这几天接了一个需求,需要实现一个动态的报表,基于reacthook+antd。这里总结一下思考和实现过程里的重点。

需求是这样的:根据后端回传的json动态生成表单,表单的绝大多数字段都是数字输入,并且有个关键需求:某些字段之间存在依赖性,例如field3=field1+field2。需要在被依赖字段更新时自动计算。

除此之外还有个问题,后端提供的json只包含了必要的属性,比如字段名、字段label、字段的依赖关系,但是一些个性化的配置,比如某些字段需要禁用、某个表单项的placeholder之类的就给不了了。

ok,接下来一步步的分析如何设计这个动态报表。相关代码已经上传至codesandbox-自动计算的动态报表,欢迎试玩~

第一步:需求分析,明确动态的边界

有很多同学一听不就是动态么,.map+jsx遍历一把梭不就好了,然后直接上手开始写。这样基本后面都会小重构个几次。写好代码的第一步还是要心里把所有代码都跑通了再开始动手。特别是对于我们这种边界很不清晰的“动态报表”,在跟负责人确认之后,明确了几个核心功能点:

字段的依赖自动计算:核心功能,并且要注意精度问题。

表单结构简单:所有表单项都是单行输入框,也不存在表单项里套表单项,或者占地面积大的表单项。

可折叠的表单项:对于一些自动填写,或者从外部数据来的表单项,可以默认折叠来减少显示的表单数量。

好,明确了这几个点之后,我们就知道动态的边界在哪里了,我们这次要做的表单是允许表单项相互关联、支持折叠部分字段、只需要满足基本样式需求的表单。如果有一天一个同事过来对你说哎呀你也太菜了,连xxx都实现不了还叫什么动态表单,这时候就可以叫他直接爬了。

这里要明确一点,动态表单肯定是自由度越高越好。但是业务实现还是要衡量工作时间,如果已经有了相关的组件沉淀,那么直接拿来用或者二次封装显然是更好的。但是如果需要从头写的话,对于不必要或者未来才有可能出现的需求,我们可以选择更灵活一点的方法来“兼容”,具体设计下面会提到。

第二步:设计组件,隔离“脏东西”

文章一开头也提到了,后端只能提供一些必要的表单配置项,而那些更个性化的配置项没办法给,比如现在所有的表单项都是必填的,突然有一天有一个表单不必填了,难道数据库整张表还要再给你加个required字段?

那么既然要前端写死,我们就要开动一下小脑瓜了,我们都知道当一个表单变动态之后,那其中表单项的设置就都要从最初的配置项json中来。但是仔细想想,对于动态表单组件来说,它不需要关系配置项是怎么来的,它只做一件事,props里接收配置项,画出来报表页面。也就是说,我们可以在从后端读到配置项之后、在render动态报表组件之前,把前端写死的数据注入进去。而不是直接写死到动态报表组件内部:

通过这种方式,我们可以把写死的这部分“脏东西”独立出去,从而保持了动态报表组件的纯洁性。

然后就是第一步结尾提到的,如何兼容未来可能会出现的表单项个性化渲染需求?其实很简单,把表单项的渲染函数独立出来,也通过props传给动态表单组件,如果不传的话就用默认表单项渲染函数。记住,如果发现组件的某些渲染部分日后有可能会改,那就把这部分渲染封装到函数里,给prop加上这个入参并把这个函数设置为默认值。

再用单一职责分析一下,我们就可以得到下面这张组件依赖图:

有的同学可能会好奇为什么要拆成业务页面和动态表单两个组件。

这是因为有些功能内容是无关于表单的,比如表单默认值的接口获取、切换折叠按钮的样式位置,产品一拍脑子加上的一些其他功能等等。这些内容和我们的动态表单是无关的,所以需要把这部分“动”代码从“静”的表单组件里分离出去,来提高功能的可复用性。

第三步:准备就绪,开始写码!

OK!设计完成开始写码!注意,下面包含的代码只是用于讲解思路,详细代码在codesandbox-自动计算的动态报表,边读文章边看例子效果更佳哦,你可以在/FormPage/service.js看到“后端”给过来的表单配置项json是什么样子的。

1、实现自动计算hook

首先让我们把精力放在核心需求:依赖项的自动计算上。在上图中可以看到是分了两个模块,模板表达式计算里是纯粹的纯函数算法,这部分的具体介绍可以看我的这篇文章使用js实现加减乘除模板计算,上面例子里已经包含了这部分的代码,这里就简单介绍一下流程:

strToToken:将后端提供的依赖表达式字符串解析成token数组:即s1+s2-100=>['s1','+','s2','-',100]。

tokenToRpn:将token数组修改为后缀表达式数组,纯算法,没什么好讲的。

calcRpn:传入后缀表达式数组和kv对象。将后缀表达式中的“变量名”替换成kv对象里的实际值,然后执行计算,计算支持加减乘除四则,由big.js提供精度修复。

而自动计算hook则是在上面这些工具函数的基础上进行封装,让其可以应用在react函数组件里。我们现在来实现一下这个。这个hook应该接受表单配置项、表单初始值、表单ref作为参数,并返回表单数据变更listener和包含自动计算结果的表单初始值。我们从返回值开始来推导为什么需要这些参数。

首先,需求是“当被依赖项更新时自动计算”,那么有且只有在form的数据发生变化时才需要检查有没有依赖项变更并运行计算,所以说我们让hook返回form在触发onValuesChange时需要的回调函数即可。

那"包含自动计算结果的表单初始值"是啥?表单初始值好理解,就是赋值给form的initialValue。但是我们不能直接把后端返回的初始值直接扔给form,因为此时还没有经过onValuesChange计算依赖字段值!所以我们需要先运行一遍自动计算,并把自动计算的结果当作初始值的一部分一起传递给form,这时才不会出现“所有被依赖字段都有值,但是自动计算字段是空的”的问题出现。

于是,hook的骨架就出现了:

/***自动计算hook*@param{object}layoutInfo表单配置项*@param{object}initialValues初始值对象*@param{Ref}formRef表单操作ref*/exportconstuseAutoCalc=(layoutInfo,initialValues,formRef)=>{//表单里具有依赖项的字段constdependence=useMemo(()=>{//从layoutInfo中提取依赖信息},[layoutInfo]);//用初始值进行首次自动计算constinitialValuesWithCalc=useMemo(()=>{constautoChangeValues=/**调用自动计算函数,并传入依赖项和默认值进行计算*/;return{...initialValues,...autoChangeValues};},[layoutInfo,initialValues,dependence]);/***监听表单字段变更,更新自动计算字段*@param{object}changedValues变更的表单数据*@param{object}allValues表单里的所有数据*/constonFormValuesChange=useCallback((changedValues,allValues)=>{constautoChangeValues=/**调用自动计算函数,并传入依赖项和本次变更值进行计算*/;//把新的数据更新会formformRef&&formRef.setFieldsValue(autoChangeValues);},[dependence,formRef]);return[initialValuesWithCalc,onFormValuesChange];};

可以看到大致分为三部分:先提取依赖信息,根据依赖信息和初始值进行首次自动计算,然后根据依赖信息和formRef新建onVluesChange的回调函数,这里对所有的东西都进行了缓存,因为一个表单内的依赖信息很难发生改变,并且这些计算开销都比较昂贵。

从上面的hook里我们可以找到两个需要实现的地方:收集依赖信息和根据依赖信息自动计算。

收集依赖信息其实很简单,遍历后端提供的表单配置项中的所有formItem,找到其中有依赖性表达式字符串的formItem,然后用上面提到的strToToken和tokenToRpn进行解析,最后生成一个如下数组:

{//字符串,要自动计算的字段名target:'result',//字符串数组:这个字段依赖于那些字段值sources:getDependKeys(tokens),//字符串数组,计算所需的后缀表达式token数组。rpn:tokenToRpn(tokens)}

注意这里我们预先解析好了计算所需的后缀表达式而不是直接保存表达式字符串,之后自动计算的时候就可以直接拿来用而不需要重复的进行解析。

根据依赖信息自动计算这个就稍微复杂一点,因为依赖项有可能是复杂嵌套的,比如下面这样:

想要计算字段E的值,先得有字段C的值,而计算字段C的值则需要字段A和字段B的值。也就是说,我们需要递归执行自动计算来解决这个问题,具体流程如下:

遍历上面收集到的依赖信息,找到那些字段需要进行计算

对这些字段进行求值

检查这些新计算出来的字段值是否会导致新的字段需要计算

如果有的话,那新计算出来的字段值进行递归运算

这部分完整的代码在DynamicForm/useAutoCalc.js-CodeSandbox,看起来很复杂但是加上hook本体总共也就一百多行,而且行为比较纯,结合例子很容易就可以理解。

2、实现表单项渲染函数

现在最难(相对来说)的部分已经结束了!接下来让我们写一些轻松的:表单项渲染函数。

这个之前也提到了,它是一个函数,接受表单配置项,返回表单的jsx。简单,策略模式梭一把:

/***渲染动态表单的表单项组件**@param{object}formItemInfo要渲染的表单项配置*@param{boolean}showCollapse是否显示该表单*/exportconstrenderFormItem=(formItemInfo,showCollapse)=>{const{compType}=formItemInfo;constrender=compTypeinformItemRenders?formItemRenders[compType]:formItemRenders.InputNumber;returnrender(formItemInfo,showCollapse);};/***表单项compType到实际渲染函数的映射*/exportconstformItemRenders={//日期输入组件DatePicker:(formItemInfo,showCollapse)=>(/**...*/),//数字输入组件InputNumber:(formItemInfo,showCollapse)=>(/**...*/),,//字符串输入组件Input:(formItemInfo,showCollapse)=>(/**...*/),};

可以看到,这里不仅提供了表达你的配置项,还提供了一个showCollapse用于指定当前是否要展示折叠的表单项(即外部的展开折叠项switch按钮)。

还有一个需要注意的地方是,当表单项被折叠时应该怎么处理,我们来看看下面这两种做法:

//折叠时不渲染(formItemInfo,showCollapse)=>{const{collapse}=formItemInfo;if(!showCollapse&&collapse)returnnull;return(<Col><Form.Item><Input/></Form.Item></Col>);}//折叠时hide(formItemInfo,showCollapse)=>{const{collapse}=formItemInfo;return(<Col><Form.Itemhidden={!showCollapse&&collapse}><Input/></Form.Item></Col>);}

两种都可以实现需求,但是哪一种更好呢?

有编程经验的同学会直接指出是第二个,因为采用hidden方式的组件不需要重复销毁创建,相对性能更好。并且第二种还可以让form提供更好的字段校验和使用体验。因为,如果我们在不显示时直接null掉表单项,那么form就不会对这个表单项进行校验,并且也不会保存对应的字段值,也就是说我们需要再额外useState一个对象来保存这些被隐藏掉的值,对比使用hidden方式的编码体验会差很多。

本部分的完整代码见/DynamicForm/formItemRenders.js-CodeSandbox。

3、实现动态表单组件

终于要开始写我们的核心组件!不用担心,因为上面我们已经准备好了所有需要的内容,所以现在实现起来会相当的轻松。先想想我们的动态表单组件都需要哪些props呢?

表单配置项layoutInfo和表单初始值initialValues是必备的,有这两个我们才能使用上面设计好的useAutoCalchook。除此之外还需要一个表单提交回调onFinish,当我们点击提交按钮后将触发这个回调把结果丢给外层组件进行下一步操作。还要有个可选项表单项渲染函数renderFormItem,就是我们上一小节完成的这个,为了节省组件调用时的代码量,我们需要将其设置为默认值。哦对了,还需要一个是否展开折叠项showCollapse,我们需要他来协助渲染表单项。

实际上还可以继续添加prop来提高灵活度,例如渲染表单小节头部、渲染表单提交按钮区域等,这里只包含了一些必要的prop。

有了这些prop,我们很轻易的就可以写出来组件的逻辑(毕竟这部分就是文章开头时一把梭会写出来的代码),这里只提一下大致的逻辑:

引入useAutoCalc,并将返回的回调和初始值设置给form组件。

给保存按钮添加一个loading

加个判断,当一个小节里所有字段都隐藏时,隐藏这个小节的标题。

遍历配置项渲染小节和表单项

添加保存按钮

完整代码见DynamicForm/DynamicFormView.js-CodeSandbox。

4、实现业务页面组件

动态表单组件搞定了,那么我们就可以在实际业务中调用这个组件了。这个业务组件里的内容更简单了:

useEffect访问接口拿到表单配置项和默认值

将前端写死的数据注入进表单配置项

添加一个展示全部数据的按钮

调用上面的动态表单组件

当然,当动态表单组件有了足够的沉淀之后,这个业务页面才会是我们开发的主战场,称其为最重的组件也毫不为过。

完整代码见FormPage/FormPageView.js-CodeSandbox,注入写死数据的函数则在injectStaticData.js-CodeSandbox。

写在最后

写到这里,我们本文重讲述的动态报表就已经开发完成了,通过合理的规划和设计,绝大多数的职责都被解耦到了不同的文件模块中。每个函数的长度均保持在二三十行左右。再加上一些必要的思路注释,~一道大餐就此完成~。

其实本文中除了纯粹的写码,也有一部分内容是如何和同事打交道,包括最开始的某些字段后端无法提供需要前端自己写死存起来的问题。后端可以存么?可以,但是会很麻烦,最后导致工期拖延,然后就要开始加班,后面还要承受后端同学的白眼,这真的值得么?

对于业务需求来说,目标是最终的交付。

而对于交付来说,如何~少加班~将开发时间分配到具体的任务上则是重中之重。而我们平时所学的设计模式、编程知识经验,不应该成为桎梏束缚住我们的思想,而是应该在面对一些不得不妥协的坏需求时,可以拿来协助我们相对更加优雅的进行解决。

当然,这是在时间不够的情况下,如果有足够的时间,必须是怎么规范怎么来,不然现在偷的懒最终都会还回去。

声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
tplink无线扩展器怎样重置密码 扩展器原始的密码是什么 如何重新设置TPLink扩展器的密码简单步骤教你修改TPLink扩展器的... 为什么要加入tcpip协议 父母如何给孩子做一个好榜样 父母应该如何做孩子的榜样? 冬季草原防火安全知识 冬季景区该如何预防火灾 厨房暗管漏水 多少钱 手脚出汗,睡眠不好,早泄是阳虚还是阴虚 ps中鼠标怎么变回箭头ps中鼠标怎么变回箭头形状 js,通过表单元素前面的字,获得这个表单元素 ps鼠标光标怎么恢复成箭头ps鼠标光标怎么恢复成箭头状态 家具用的什么木板材质最好 家具用哪种木板材质 家具买什么材质的好 家具材质有哪些 usb耳机怎么用 深圳大学建筑系怎么样?要多少分才能上啊?广工的呢? 怎样用手机查自己还剩下多少条信息的 沈阳沈河区东贸路距离南塔多远 萃苑社区社区概况 物业公司注册资金600万,用物业公司营业执照可以申请消防维保二级资质吗... 物业有营业执照无资质证明参加投标但拿了资质才签合同 ufs2.1和ufs3.1区别是什么? 手机UFS2.1和UFS3.1差别大吗 我想要在我的微信公众号中(服务号)设自动回复功能,为什么不行? 这种公众号自动回复怎么做的?求! 新保温杯怎样处理一下才可以喝水保温杯杯盖有胶味怎么去除 这个是保温杯里面的东西,洗洁精,钢丝球都洗过了,洗不掉,请问拿这个喝水... 纯中药降压药有哪些 有什么中药性质的降压药吗 以前用的是紫舒胶囊 现在那药没有了 还想... 宁波社保扣除的19.79元多是什么? 宁波社保卡扣19的是什么钱? ...店做斯巴鲁的销售人员,这车怎么样,发展前景怎样?这是我一生的命运... 离婚冷静期可以买房吗? 离婚冷静期买房属于谁? 离婚冷静期买的房子算个人财产吗 离婚冷静期有几天 在离婚冷静期买房属于谁 离婚冷静期买房怎么分 离婚冷静期购置房产属于共同财产吗? 想问一下离婚冷静期买的房子算夫妻共同吗? 房事出血怎么回事 房事后阴道出血怎么回事 房事之后出血是怎么回事 房事后阴道出血该怎么办 根号2计算公式? ...很干净也很安静的坐在那里,大花猫是以前家里养了? 我梦见两只花猫,都是黄黑白的颜色,一只猫在我身边没事,我却眼睁睁地... 联想W510特点 联想W510详细配置 联想W510 4319-A29的显卡性能如何? Thinkpad W510凭什么卖到28000元?