基于React的无限表单设计

前言-问题定义

需求定义:通过定义符合一定规范的JSON格式的数据模型,可以生成相应的表单组件并返回结构化的表单数据。
包含可扩展的基础原子组件和布局组件,并且可以自由控制嵌套层级,在一定程度上达到最优的渲染效率。

问题分解(TLDR):

表单组件模型定义:表单生成器模型

无限嵌套组件设计:表单结构设计树形数据

同级节点链接问题:控制/依赖关系转移到父组件

状态管理问题组件:受控和非受控组件的选择可控

优化数据更新问题:不可能变量数据优化+数据更新检测

节点数据更新问题:使用Reducer模式

1.定义表单组件模板

JSON表单生成:使用JSON格式描述组件外观样式,控件类型和控件扩展属性通过翻译信息生成界面组件JSON格式的描述,可以提供一些交互功能(例如组件之间的链接功能)。

例如有一个基本的表单结构:

{"label":"username","key":"userName","value":null,"default":"Stephen","type":"Input"//这里根据Antd控件给出定义。
其他类型如:CheckBox、Select、Group、Tabs、CustomA等自定义组件

常规组件类型(本文以Antdesign组件为例^1)。
常规组件如CheckBox、Select、Input等,我们经常需要一些布局组件如CollapsibleMinimizePanels、Tabs等。

自定义组件类型,自己创建的,为了符合表单模板的结构,它可以是基于业务系统的组件,也可以是基于交互的组件,也可以是可以是基本组件(例如字体组件)的扩展。
带有字体颜色、粗细等配置项。

2.无限嵌套的组件设计

有了基本的组件表单模板后,如何通过表单定义,如搭积木,更自由地组装出想要的结构,并可以无限扩展?足够的基本元素和基本元素类型。

无限嵌套:有很多函数可以实现无限嵌套。
最常见的是树列表控件。
这里也使用树结构作为无限嵌套的基础。

扩展表单模板:

{"label":"用户名","key":"用户名","value":null,"default":"Stephen","type":"Enter",//...其他属性"children":[]//这里的children是表单自身结构的数组。

没有树形结构,没有什么新意或者难点设计。
稍微复杂一点的是。
数据回调的更新和树组件渲染的优化将在后续章节中进行扩展。

3.兄弟节点绑定问题

节点绑定兄弟:从业务需求角度来看,组件之间存在一定的“约束关系”。
更改组件A会影响组件B的外观或值的范围和。
其他因素。
对于树组件来说,这种约束关系往往存在于“同一层级”,交叉组件之间很少存在链接关系。
如果存在,可以判断是功能划分还是跨组件扩展问题。
依赖支持。

由于这是兄弟节点之间的关系问题,所以最好的解决办法就是“调度提升”到父组件,由父组件“协调”组件子组件之间的关系。
另一种可能的解决方案是子组件监听目标子组件的值,从而影响其自身的变化。
对于简单的需求,可能没有对错之分,但还有其他因素值得关注。

分类父组件子组件协调依赖监控自上而下自下而上DAG检测组件模式受控组件模式非受控组件模式

本文主要采用父组件的子组件“协调”方式,主要原因是可以有更灵活的控制,充分利用非受控组件的优势。
受控组件和非受控组件之间的权衡将在稍后讨论。

在扩展表单模板之前,我们需要确定实现功能的几个因素:

是...依赖组件的Key值

功能改变/评估函数fn

子组件本身可以改变的值或外观

首先,对于依赖组件ComponentKey,我们可以通过设置deps来指定依赖组件的key值。
有了Key值,我们就可以得到依赖组件的当前值,如果需要更高级的转换,我们可以使用自定义函数f进行转换,最后返回一个具体的属性值来修改自身。
这是一个常见的例子。
组件A“ShowLabel”是一个复选框组件。
当启用ShowLabel时,组件B的输入可以启用编辑模式,否则不允许编辑。

注意:此处的Javascript文件扩展名是为了更容易显示。
使用上,简单的JSON数据格式不支持函数注册,需要手动转换。

扩展模型如下:

[//A组件{label:"ShowLabel",type:"Checkbox",key:"enableShowLable",default:false},//B组件{label:"LabelContent",type:"Input",key:"enableShowLable",disabled:true,watcher:{deps:["enableShowLabel"],action:props=>{return{disabled:!props.enableShowLable};}}}];

下一步是父组件的设计。
详细的说明可以查看JSDoc。
:

/***获取子组件依赖的值,传递给子组件*@param{deps:[],aaction:Function}子组件的watcher-watcher属性*@param{Array}Children-所有当前兄弟节点*@returns{any}*/constgetDependencyValue=useCallback((watcher,children)=>{if(watcher?.deps){//注:onlysupportdependononepropertyfornow.constdependencyKey=watcher?.deps?.[0];returnchildren?.find(r=>r.key===dependenencyKey)?.value;}},[]);/***获取子组件的action修改的属性内容*@param{Function}-该子组件的观察者属性subcomponent*@param{string}key-应更改的属性他自己。
键值,如“disabled”*@param{any}value-当前依赖节点修改的值*@returns{any}*/constinvokeDependencyWatcher=(action,key,value)=>{if(!action){return{};}constitem=action({[key]:value});returntem;};

注意:独立组件本身由父子组件的值和操作。
这里没有实现的是DAG依赖检测。
以及子组件更新自身属性值的“安全性”。

通过以上内容,一个自定义表单组件构建器的基本结构就基本完成了。
接下来,我们来讨论一下自定义表单的设计和优化。

4.组件状态管理问题

关于“状态管理”这个话题,我不想展开太多。
这里需要讨论的是原子组件的设计是否是内部自维持的。
组件或完全依赖于外部输入。
各自的优缺点是什么以及如何选择。

4.1什么是受控组件

在HTML中,表单元素(例如input、textarea和select)通常会根据来自表单“user”的输入来维护自己的状态和更新。
在React中,可变状态通常存储在组件的state属性中,并且只能使用setState()^2进行更新。
在大多数情况下,我们建议使用受控组件来处理表单数据。
在受控组件中,表单数据由React^3组件管理。

简而言之,我们对如何使用组件拥有完全的自由和发言权。
以React组件为例,受控组件意味着你必须手动绑定值,并在onChange后更新值才能再次绑定,例如:

handleChange(event){this.setState({value:event.目标.值});}render(){return();}4.2什么是非受控组件

因为非受控组件将会是true数据存储在DOM节点中,因此当使用不受控制的组件时,有时集成React和非React代码会更容易。
如果您不介意代码美观并希望快速编写代码,那么使用不受控制的组件通常可以减少您编写的代码量。
否则,您必须使用受控组件。
^3

与受控组件不同,非受控组件只能关心defaultValue的默认值和最终结果的变化值。
他们不关心内部包装加工是如何完成的,他们也能做到。
什么时候触发的数据发生变化并不重要,但是当你需要知道自己组件的状态时,获取值时

从组件使用者的角度来看,有以下区别^4:

featureuncontrolled获取控件值检查控件值变化格式化输入内容动态更改值支持外部触发刷新渲染内部刷新组件是可控的吗??

从组件设计者的角度来看,有以下区别:

不受控制的功能需要编码来维护状态??更改状态的延迟存储??

总结一下,这里解释的是什么?定义和与不受控制的组件的区别。
对于设计“无限”级联表单,“支持外部触发刷新渲染”、“动态改变数值”、“内部组件刷新是否可控”,这些因素会特别限制组件的模式选择。
对于组件树中不是叶节点的组件,受控组件是更好的选择。
当然,对于叶子节点组件,如果业务逻辑复杂,存在“大量”的临时状态数据(这里业务逻辑复杂会产生大量的临时状态,根据个人设计经验,如果没有确切的可测量的指导值,请自行决定),封装一个面向私营公司的自定义组件更合适。

5.优化数据更新问题

这里就出现了一个问题,组件树,经常会改变某个分支的节点,但其他分支却没有更新。
如何才能实现“局部”刷新呢?非全局刷新,无谓消耗浏览器性能。

这个问题的本质在于,当组件监听自身依赖的值(比如组件输入的值)时,如何判断组件是否“改变”。
对于简单类型,例如字符串和数字类型,我们可以依赖值本身发生变化的事实,但对于对象类型,情况有点复杂:

对象中包含的值是一个原始。
type(stirng,number,h3int,boolean,non-unified,symbol...)^5

对象中包含的值是Object类型。
层次很深。
穷举对象的原始类型并不是特别容易。

问题1自然很容易解决。
对于第二个问题,需要使用Immutable数据结构,并结合ImmutableTools来比较对象的值是否发生变化。
这里我们以ImmutableJS为例^6。
还可以选择ReduxJS选择的ImmutableImmerJS^7数据结构库。
两者的实现原理略有不同。

导入{Map,is}来自“不可变”;constmap1=Map({a:1,b:1,c:1});constmap2=Map({a:1,b:1,c:1});//比较方法map1===map2;//falseObject.is(map1,map2);//falseis(map1,map2);//true

至此,我们就有了区分数值变化的工具和方法依赖。
您需要从React收集备忘录来确定是否更新组件本身。

//...imroteratedresouce//这个函数相当于`shouldComponentUpdate`constcomponentShouldUpdateFn=(immutablePrev,immutableNext)=>{returnimmutablePrev===immutableNext;};constMyComponent=memo(((({nameObj:{firstName:string,lastName:string,middleName:string}})=>{return
{Hello`${nameObj.lastName}`}
;},componentShouldUpdateFn);

这里简单实现了如何使用Immutable+ReactMemo实现部分组件刷新,如何更新数据就不写了。
这里可以自行参考ImmutableJS或者ImmerJSAPI

6.节点数据更新问题

无限极组件更新自己,需要更新数据集才能共享状态,主要问题是组件如何定位。
树节点。
可行的解决方案是,在创建组件时,根据组件父子组件之间的键层次关系,获取子组件的唯一索引KeyChains,如['group1','label','showLable'],对应的树形字符串是:

-group1-label-showLabel-labelContent-group2....

更新某个值时子组件,您只需要传递KeyChains和修改后的值即可。

另一种常用的解决方案是基于KeyChains将树形结构扁平化为单层文件结构,通过将KeyChains转换为Key字符串来进行快速搜索,例如“group1.label”。
showLabel”并更新。

总结

这篇文章希望能够给其他人带来启发,得到更多的建议和思考。
形状生成组件设计行业也有很多成熟的产品和设计思想。
这里总结了一些项目中遇到的一些问题和解决方案,希望能针对具体问题提供进一步的思考和探索。

目前还有一些空白没有填补,比如原子组件布局设置、自定义原子组件的动态注册等,后续会更新。

开源流程引擎哪个好,如何选型?

市场上著名的开源流程引擎有osworkflow、jbpm、activiti、flowable和camunda。
其中jbpm、activiti、flowable和camunda这四个框架有一个共同的根——jbpm4。
一旦开发人员熟悉其中一个框架,他们通常可以轻松掌握其他三个框架。
在开发低代码平台、OA系统或BPM软件时,流程可视化功能至关重要,而该功能的实现依赖于流程引擎和流程设计器。
面对市场上众多的开源流程引擎,如何选择功能和性能优异的引擎呢?一、流程引擎选型1、Osworkflow:Osworkflow是一个基于状态机机制的轻量级流程引擎,数据库表较少。
它提供了步骤、条件、循环、分支、合并等基本元素,但不支持连署、跳转、返回、加签名等操作,需要开发者自行扩展。
对于简单的流程,Osworkflow是一个不错的选择。
例如,2008年笔者为某大型国有企业集团开发OA系统时,就是基于Osworkflow的。
目前仍运行稳定,性能较高。
2.JBPM:JBPM是JBoss公司开发的,目前最高版本是JBPM7。
从JBPM5开始,它与以前的产品有所不同。
建议不要选择JBPM5之后的版本,因为基于DroolsFlow的技术在国内市场上很少使用。
JBPM4是早期版本。
后来,其创建者TomBaeyens离开JBoss后,加入了Alfresco,并推出了Activiti,一个基于JBPM4的全新开源工作流系统。
另外,JBPM使用hibernate作为数据持久化ORM技术已经不再是主流。
例如,笔者在2012年为某集团开发BPM平台时,选择了JBPM4.4版本,并进行了大量的扩展开发,以实现具有中国特色的流程需求。
现在来看,JBPM并不是最好的选择。
3.Activiti:Activiti是AlfrescoSoftware开发的,目前最高版本是Activiti7。
Activiti的版本比较复杂,包括Activiti5、Activiti6、Activiti7等,选择时需要了解这些版本的发展历史。
Activiti5和Activiti6的核心领导者是TijsRademakers。
由于团队内部存在分歧,TijsRademakers于2017年离开团队并创建了Flowable。
Activiti6和Activiti5的代码已移交给Salaboy团队,官方已暂停维护。
Salaboy团队目前正在开发Activiti7框架。
其核心依然基于Activiti6,并没有注入更多新功能。
它只是Activiti之外的上层封装。
因此,Activiti应该谨慎选择。
4.Flowable:Flowable是源自Activiti6的版本。
最新版本是v6.6.0。
Flowable修复了Activiti6中的许多Bug,并在此基础上添加了DMN和BPEL支持。
与开源版本相比,其商业版本功能更加强大。
从Flowable6.4.1版本开始,Flowable大力发展商业版产品。
开源版本没有及时维护,部分功能在开源版本中不再发布,比如表单生成器(表单引擎)、历史数据同步到其他数据源、ES等。
Flowable是一个用Java编写并使用ApacheV2license协议开源的轻量级业务流程引擎。
2016年10月,Activiti工作流引擎的主要开发人员离开Alfresco,开始基于Activiti分支的Flowable开源项目。
Flowable项目包括BPMN引擎、CMMN引擎、DMN引擎、表单引擎等模块。
5.Camunda:Camunda基于Activiti5并保留PVM。
最新版本是Camunda7.15,保持每年发布两个小版本的节奏。
Camunda的开发团队也从Activiti中分离出来。
它的发展轨迹与Flowable类似,也提供了商业版本。
对于一般的企业应用来说,开源版本就足够了。
笔者强烈推荐Camunda流程引擎,并在云城低代码平台中使用Camunda。
其功能和性能稳定。
2.流程设计器选择对于低代码平台的流程可视化,流程设计器是一个重要的支撑工具。
目前市面上主流的流程设计器有bpmn-js、mxGraph、Activiti-Modeler、flowable-modeler、easy-flow、bpmn2-modeler插件等。
1.bpmn-js:bpmn-js是BPMN2.0的一个渲染工具包和网络模型。
bpmn-js正在努力成为CamundaBPM的一部分。
使用Web建模工具,您可以轻松构建BPMN图并将其嵌入到项目中以方便扩展。
bpmn-js基于原生js开发,支持在源码框架中集成到vue、react等。
2.mxGraph:mxGraph是一个功能强大的JavaScript流程图前端库,可以快速创建交互式图表和图表应用程序。
由于mxGraph是一个开放的js绘图开发框架,我们可以根据项目需要开发很酷的样式或完全定制它。
3.Activiti-Modeler:Activiti开源版本自带网页版流程设计器,Activiti-explorer项目中有Activiti-Modeler。
优点是集成简单,开发工作量小。
缺点是界面不美观,用户体验差。
4.flowable-modeler:flowable的开源版本还附带了流程设计器的Web版本。
显示风格和功能与Activiti-Modeler基本相同。
优点是集成简单,开发工作量小。
缺点是界面不美观,用户体验差。
5.easy-flow:码云上开源的流程设计器,尚未深入研究。
感觉和真正的BPMN流程图设计还有一定差距,但至少有框架了。
6.Eclipse插件bpmn2-modeler:流程设计器的C/S版本。
如果不强调基于浏览器设计流程图,也可以考虑Eclipse插件版流程设计器bpmn2-modeler。
3.选型建议:建议您使用Camunda(流程引擎)+bpmn-js(流程设计器)的组合。
笔者在运城BPM及多个项目中进行了实践验证。
Camunda在功能上比Flowable和Activiti流程引擎更强大,性能和稳定性也更突出。
经验系统:。