不积跬步,无以至千里;不积小流,无以成江河。 文章我就喜欢读长的,表单我也要处理大的。 近一年先后经历过的两个项目都有大的复杂表单处理需求。 走过路过,没有错过,借着经验留下点足迹。
提纲:
- 问题
- 怎么做
- 举证
- 代码编写过程和过程中发现&处理的问题
- 升级为Hooks方案后需要的改进
- 总结
表单处理是前端比较常见的需求,尤其是B端。表单处理大概可以分为基础表单(常见于数据项较少的表单场景)、分步表单(将一个冗长或用户不熟悉的表单任务分成多个步骤,指导用户完成)、比较复杂的高级大表单(根据开发业务的需要,进行大批量的、类型繁杂的数据处理表单)。这里结合我做的项目,介绍下比较复杂的大表单怎么处理。
问题
当表单项足够多时我们可以选择分步表单处理,但是当表单项之间关联或者说联动也比较多时就不是简单的通过分步就能化繁为简的了。我处理的项目多是不能通过分步简化的,所以就有了处理复杂大表单的需要。
那么如何实现一个可拆分复用、易操作伸缩、能组合通用的复杂大表单?
怎么做
可拆分复用: 大表单首先必须要能进行拆分,我们通过一套自定义约定来进行父子组件的通讯,以满足拆分复用的需要。这里我们约定父组件调用子组件时要传入values & onChange属性,作为子组件的渲染数据和父子组件通信函数使用。拆出来的公共部分就可以复用。
易操作伸缩: 复杂表单数据处理的过程中经常需要Modal或者Drawer的协助,我们可以把Modal或者Drawer中的表单元素用所用组件库的Form组件进行包裹,让这个Form收缩成父组件的元素——我称之为Form as Form.Item,这样就不用每次子组件某个数据变更时都onChange通知父组件了。我们可以在对Modal或者Drawer进行确定、提交操作时,在父组件中通过Form引用获取子组件的Form数据,这在代码逻辑上更加易于操作。当然Modal或者Drawer本身包含的是最外层的表单不用。
能组合通用: 需要我们能把拆分的组件或者组件库组件根据需要组合成一个新的组件后,仍能按照上边两条进行操作处理。其实这个过程最终会变成父组件对子组件传递的各种情况数据的支持,需要父组件能通配处理简单的原始类型、普通对象、普通数组、组合的复杂对象数组等。
举证
可拆分复用:
下边为摘出来的部分模块需求,我们拆分出来了规格列表SpecList组件,父组件调用时传入了渲染数据(供初始化或者数据变更渲染)和通信函数(onChange),子组件在数据变更时调用通信函数,数据到了父组件可以继续向上,层层反馈到最终的Form供表单提交使用,也可以用于和其他组件作数据联动(这个组件其实就和下边的SkuTable组件有联动),拆出来的组件有需要可以在多个地方复用。
1 | /* |
父组件调用
1 | <Form.Item label="规格"> |
易操作伸缩:
上边SpecList组件中的Modal包裹了这个组件,点击确定或者提交时,Modal就可以在onOk属性定义的this.handleSubmitItem函数的调用中,通过引用获取下边SpecEditor组件里Form表单的内容。弹窗里边的Form就相当于收缩成了外边Form的Form.Item,有拆解需要的拆解as Form.Item的Form就是伸展过程。
1 | /* |
能组合通用:
下边Table是由<InputNumber />
、<Input />
、<Select />
等组成,Table产生的数据是由对象组成的数组,对象由字符串、数字组成。当子组件产生其他形式的数据组合时,父组同样可以处理,这样的组合可以非常灵活,任何你想要的组件或者数据的组合都可以得到支持。
1 | /* |
父组件的调用:
1 | <Form.Item label="价格与库存"> |
代码编写过程和过程中发现&处理的问题
- 拆分组件:比如上边拆分的SpecList规格组件,SkuTable价格与库存组件,SpecEditor组件等
- 初始化组件:需要后台数据做初始化的,请求后台接口,获取到数据后把数据拆合处理成前端需要结构的数据,再分配到对应的组件,不需要的传空数据或者视情况传递需要的数据
- 拆分的组件对各自的数据做个性化处理
案例:
SkuTable 组件产生数据是个list,处理list item数据时触发的动作有:编辑、删除;添加、删除;保存、取消 - 和父组件通信:拆分的组件,更新数据时,验证通过后通知父组件(onChange);删除、编辑数据(会更新数据的编辑状态)时通知父组件(onChange),父组件收到数据后作进一步的传递和联动,未拆分组件部分的数据在总表单提交时获取。
- 提交表单:汇总表单未拆分出组件部分和拆件为组件部分的数据,作必要的校验,按接口结构组合数据,发起请求,重置表单等。
问题1: 上边动作的处理过程中,对item的定位,交互展示,编辑态等该怎么处理?
答: 对新增数据(newItem)或初始化的数据加入额外标识字段方便管理
比如加入这些字段 {key: ‘NEW_TEMP_ID_${this.index}’、isNew: true、editable: true}。key
: item定位唯一标识符。list数据增删会造成item定位混乱,给list item加key后无论如何增删编辑item,都可以通过key对item准确定位 。isNew
: 是用来区分交互的。true为添加item,false为修改item。editable
:标识是否正在编辑item数据。在进入编辑状态时要cache item原始数据,这样取消编辑还能找回;如果有item数据还处于编辑状态,则无法提交表单,提交时给出相应提示。
问题2: 数据校验应该是什么思路?
答: list item数据在保存时就要进行合理性验证,分条逐级校验,这样可以避免表单整体提交时大范围校验的复杂度。
问题3: 拆分的组件通常包含的方法有哪些?
答:
常包含的方法有增加、删除、修改、保存,
有Modal的话在父组件中还会有 submit行为处理 和 取消submit行为处理的方法,
通用的list item定位方法(eg. getRowByKey)。
下边列出SkuTable组件渲染之外的代码,供参考:
1 | /* |
升级为Hooks方案后做的一些改进
下边我在子组件中主动对父暴露了两个方法(就是说没有通过onChange方法和父组件通信),这样的话父组件可以像前边的Modal一样通过子组件的引用调用到子组建暴露的这两个方法,这样做子组件和父的耦合更小,封装的更好。
函数组件给父组件暴露方法的方式和类组件不同,核心思路如下:
1 | import React, { useRef, forwardRef, useImperativeHandle } from 'react'; |
总结
通过上边的讲解,我相信你已经有了应对大表单的处理思路。实际上上边方法的扩展性很强,理论上可以处理任何大的表单,所以你准备好了吗😊。