基于分步表单的实践探索

来源:博客园 2023-07-11 10:41:38
x

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。。


(相关资料图)

本文作者:修能

以下内容充满个人观点。◡ ヽ(`Д´)ノ ┻━┻

前言

基于分布表单的需求,在中后台管理中是一个非常常见的需求,通常具有如下布局:

其中,自定义需求度从高到低为,正文 > 按钮区 > 步骤条。

虽然布局类似,但是实现的方式却是天差地别,这里就探究一下究竟怎么样实现可以兼具代码的可维护性和可读性呢?

指出问题Container

我们这里,以「指标-数据模型」的代码为例。

首先先来看看数据模型这里的代码是如何实现的?

export default () => {  ...  return (    <>      
{["tab1", "tab2", "tab3", "tab4", "tab5"].map( (title, index) => ( ) )}
{stepRender(current, { childRef, modelDetail, globalStep: globalStep.current, mode, isModelTypeDisabled, setModelDetail, setDisabled, onModelNameChange: handleModelNameChange, })} ...
{current === EnumModifyStep.tab1 ? ( ) : null} ...
)}

这是数据模型编辑页面 Steps所在的容器组件的 DOM 部分的代码。

可以看出来,设计者的思路是比较明确的,通过 header,content,和 footer 进行分层, 增加代码的可读性。

在 header 中,通过声明 title 数组的方式创建 Steps 的方式简洁又不失可读性。

在 content 中,有几个问题的存在:

既然 header 和 footer 都有语义化的标签强化可读性,我认为这里其实也可以添加语义化的标签强化可读性,譬如 main或者section,当然同时还需要考虑会不会造成过深的层级。stepRender函数的实现把一大堆 params传到子组件是否合适。为何 content 区域内,会存在 Modal?对于没有设置 getPopupContainer的 Modal 来说,其会通过 createPortal在 body 上创建,那么在这里不论是写在 content 还是 header,都不会影响它的渲染,所以我推荐把 Modal 写到最角落里,不影响可读性。在 footer 中,通过 current === 步骤的方式去定义按钮,我认为这种方式会使代码显得较为冗余。Tab1

我们这里以指标相关代码为例,以简见深,以小见大

export default (props) => {  ... const { cref, modelDetail, mode, onModelNameChange } = props;  useImperativeHandle(cref, () => {    return {      validate: () => {...},      getValue: () => {...},    }  });   useEffect(() => {     setFieldsValue({       a: modelDetail.a,       b: modelDetail.b,       c: modelDetail.c,     });    }, [modelDetail]);  return (    
... ... ...
)}

这里我想指出的第一个问题是,ref 的使用,由于 ref 无法在 props 中传递,需要通过 forwardRef 才能拿到。然而这里通过 cref 这种比较 hack 的方式进行一个操作。我认为这是一个不推荐的做法,如果需要拿 ref 我建议是老老实实通过 forwardRef 拿。

其次是 Row 和 Col 的使用,并不是说 Col 达到 24 之后就需要再写一个 Row,你可以继续写的呀,童鞋!

这里需要提出来的一个论点是,每一个子组件里去写 Form 的方式好(即上面的这种写法),还是总体写一个 Form 的方式更好?个人认为前者存在的问题如下:

由于子组件写 Form,但是提交(或下一步)按钮在外面,那么必然需要用 ref 拿到子组件的实例,并调用相关方法。(上面是 validate 和 getValue 分别对应下一步和上一步调用)没有遵循 single source of truth(单一事实来源)如果多层级结构,例如 RelationTableSelect 的话,每一层都有填写内容,那么需要大量 Form + ref,降低可维护性。

除此之外,由于基础信息比较简单,所以不存在 props 层层往下传递的问题,但是复杂组件就会存在层层往下传递的情况,那么就涉及到是否需要 context 的问题了。当然,我推荐是需要 context 的。

Tab2

这里再看一眼第二步关联表的设计

interface ITab2Props {  cref: IModifyRef;  modelDetail?: Partial;  mode: any;  globalStep: number;  updateModelDetail: Function;  setDisabled?: Function;}const RelationTableSelect = (props: ITab2Props) => {}

首先,这里需要支持的一个设计思路是,通常情况下,切忌直接把 dispatch 传递给子组件

关联表这里的设计由于层级嵌套很深,子组件非常多,导致updateModelDetail不断往下传递,你完全不知道哪层组件在什么情况下会去修改这个值!!!这对于 SSOT 来说,是毁灭性的打击。

再加上 modelDetail是一个很复杂的数据,对于可维护性来说,属于是力中暴力地打击了。

解决问题

综上,我们设计分布表单的时候,需要规避以上的问题,遵循如下原则:

SSOT可维护性可扩展性

首先实现如下组件:

这一块代码比较简单,无非就是投传几个值到对应的组件中去。

接下来考虑底部按钮的可扩展性。

通过 submitter属性支持定制按钮的交互属性。

接下来要解决按钮的事件,这里有两种方案,一种是将事件挂载在 Container 上(即这里的 StepsForm 组件),通过诸如 onCancel,onSubmit,onPrev等方式进行反馈。我认为这种方式不够好,原因有如下几点

通常我们会把子组件提出来,不会和 Container 组件写在一起,这就会使得我们需要在不同的组件中写按钮的交互逻辑和 UI 逻辑,存在隔离感有时候我们需要把 Select.Option 相关的数据一起放到数据里给到服务端,这种方式交互需要把 Option 的数据提取到 Container 中需要通过 ref 去子组件获取值

而目前我考虑通过事件订阅对按钮事件触发,通过 useEffect 监听事件,但是这种方式的缺点如下:

不够直观,和我们通常来说的组件开发有一定相悖的思路

除了以上两种方式以外,其实还有一种方式,即通过实现 Children 组件,将 Children 组件作为 StepsForm 的子组件,从而使得将每一步相关的 title 和 onSubmit 等方式都挂载在 Children 组件上。即 ant-design-pro 中的 StepsForm的实现方式。我认为这种方式的优点在于直观,不割裂。缺点在于如下:

为了获取 title 不得不先渲染子组件,从而导致 DOM 先渲染出来,然后通过 active 判断表单是否渲染。导致子组件无法通过 useEffect获取数据

其中第二点我认为是无法忍受的,这和开发组件的思路完全相悖,故摒弃这种方式暂时考虑不清楚是第一种好还是第二种好。

这里先考虑实现第二种方式后组件书写的效果:

export function () {  ...  StepsForm.useFooterEffect(    ({ prev }) => {      prev(() => {});    },    [StepsForm.PREV],  );  StepsForm.useFooterEffect(() => {    message.info("预览")  }, [PREVIEW]);  StepsForm.useFooterEffect(    ({ next }) => {      next(() => {        return new Promise((resolve) => {          setTimeout(() => {            resolve();          }, 1000);        });      });    },    [StepsForm.NEXT],  );  return (    ...  )}

hook 的实现方式也比较简单,基于事件订阅,结合每一个按钮都赋予一个唯一值。实现按钮交互触发后,通过事件分发,触发当前渲染的组件中的监听 hook。

总结

本文意在探索分步表单的最佳实践,防止不同的同学在开发该类型的需求会写出五花八门的代码,从而导致降低可维护性。

本文提到的解决方案也不认为是最佳实践,其中不同的方法经过分析都存在优点和缺点。在实际的开发过程中,仍然需要根据具体的需求进行调整。

但是基于分步表单的特性和使用场景,总结出适用大部分情况下的方法论是有必要的。

最后

欢迎关注【袋鼠云数栈UED团队】~袋鼠云数栈UED团队持续为广大开发者分享技术成果,相继参与开源了欢迎star

大数据分布式任务调度系统——Taier轻量级的 Web IDE UI 框架——Molecule针对大数据领域的 SQL Parser 项目——dt-sql-parser袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices一个速度更快、配置更灵活、使用更简单的模块打包器——ko
x

热门推荐

基于分步表单的实践探索

2023-07

张一山怒怼王思聪完整版 张一山急中生智装外国人 基本情况讲解

2023-07

港股异动 | 紫金矿业(02899)涨超4% 拟16.75亿投建新疆乌恰县萨瓦亚尔顿金矿项目

2023-07

天使歌词

2023-07

实现大规模减贫经验值得借鉴!中国对世界减贫的贡献率超70%

2023-07

亚太股份(002284.SZ):公司汽车电子产品占营收的比重近年来逐步上升

2023-07

联盟高管:文森特是与詹眉搭档的完美控卫 他让我想起查尔莫斯

2023-07

退休教授“动态模糊”摄影作品入选影展遭质疑,当事人:欢迎网友评说,这是健康的社会声音

2023-07

咨询医生男(咨询医生男科广州

2023-07

优环境、惠民生,苏州望亭镇人大组织开展雨污水管网检测修复专项视察

2023-07

推荐阅读

山西2021年度发放国家助学贷款逾29亿元 助40万名学生圆大学梦

2021-12

伪造事故赚取“差价” 机动车骗保成诈骗犯罪重灾区

2021-12

内蒙古满洲里新增确诊34例

2021-12

张家口崇礼全力做好冬奥测试赛服务保障工作

2021-12

咖啡、啤酒、盒饭……早出晚归的打工人 寒夜的便利店有故事

2021-12

云南涉疫医疗废物实现“日产日清”

2021-12

对话“贩毒”母亲:不认罪正申诉,盼抗癫痫药物氯巴占可合法购买

2021-12

甘肃省电力投资集团有限责任公司原党委委员、副总经理刘晓黎被开除党籍

2021-12

湖北省委政法委原副巡视员汪宗兴接受审查调查

2021-12

利用游戏平台设线上赌场 江苏一犯罪团伙涉非法牟利数百万元被连锅端

2021-12