这段时间陆续收到一些小伙伴的信息,对流程引擎和自定义表单比较感兴趣,内心还是比较欣喜的。多数人还是对elsa实现的流程引擎比较感兴趣,要源码,这部分内容原本是有打算把源码开源出来的,但后来发现elsa的版本升级到了2.0之后,与之前的代码相差比较远,要重构的话,前后端需要改很多东西,elsa1.x的流程流转核心部分代码设计得还是比较巧妙,能满足各种审批业务的变化需求,自己对核心部分的代码做了一些扩展,所以暂时没有升级的打算。
自定义表单部分的文章还是继续往下面写吧,这部分文章都是偏设计方面的,真正想做低代码软件架构方面的设计开发多少都有一些益处,整体工作流+自定义表单再整合前后端框架从前后设计技术研究及代码实现差不多前后花了一年左右的时间,当然都是工作之余的时间写的。
之前介绍的自定义表单中的视图定义为单一功能的封装,比如列表视图(定义普通查询区域,高级查询区域,列表操作按钮区域,行操作按钮区域,分页控件区域,列显示区域等)或者表单视图(封装表单行列定义,表单验证等)等,都是具体某个特定功能的实现。而这里介绍的表单模型,则把它定义为一个容器,容器里面会进一步定义行列,容器里面可以包含容器或者表单,每一个页面会定义唯一一个最外层的表单容器,我们可以把它看作根容器,这样就整体形成了一棵树,根节点就是最外层的表单定义,树的节点可以是子表单、视图、表单行、表单列、视图行、视图列、视图控件等,整体就可以构造出一树庞大的树。
自定义表单最终会转换为一棵树,树的话就会有树的特性,树上的每一个节点,都可以构造一个唯一的Code和PId,自定义表单中的树节点还会扩展出它属于哪个视图或者哪个表单的属性,那么这里就是引申出子表单子视图,父表单父视图的概念。有了树模型的定义,那个后面绝大多数内容都是围绕树模型来实现的,前端在渲染界面的时候,根据树节点一层一层的渲染界面,渲染界面的同时,将每个节点的Code和PId,节点属于哪个表单或者视图都会赋值到每个树节点控件中,有了树模型的定义,那么规则引擎就有了理论支撑,界面中的任何一个事件,都可以定义规则来实现自定义的逻辑(比如点击列表视图的行编辑按钮,弹出编辑人员子表单,则大致的规则引擎执行逻辑为:找到列表视图特定行编辑按钮所在的列表视图,在列表视图中找到编辑人员子表单,把当前行的Id字段取出来作为参数,用模态对话框弹出子表单,用Id字段执行后端方法获取单条人员数据,将人员数据绑定到人员表单中)。
表单模型没有具体的功能,它的作用是一个容器,它充当视图与视图之间交互的桥梁的作用,当然是通过规则引擎来串联起来的,另外表单也是页面的入口与缓存的存储数据的入口。
表单模型拆分为表单主表、表单项、表单行、表单列,关系都为1:n。常见的表单项只有一个,但像Tab布局或者有先后步骤的Step布局则会有多个。
表单主表关键字段说明:
表单列:
表单列可以存储单个控件、子表单、子视图等
自定义表单是典型的修改非常少,访问非常平凡的,系统的每一个功能都需要读取自定义表单的定义信息。为了使自定义表单不影响性能,这里采用了双重缓存设计,浏览器每访问一个页面,都会将表单和视图的定义信息存储到浏览器本地数据库中(IndexDb),应用程序后端将表单和视图的定义信息全部放到应用程序内存中,且将表单或视图的相关信息以字段冗余的方式存储到特定字段中,任何信息的改变都会重新生成新的版本号并清空内存中的缓存,前端请求页面只,会带上浏览器本地存储的表单和关联子表单子视图版本号与服务器版本号对比,版本号不同时,刷新浏览器缓存数据,再渲染页面。分布式部署中就存储缓存一致的问题,后面单独写文章来整体讲解缓存这块的实现。
自定义表单本来就是要解放繁琐的低效编码问题,但是要把一个表单配置出来,还是会花费比较多的时间,且需要对这套表单引擎比较熟悉,配置同样比较繁琐且低效,那么我们同样可以采用自定义表单的思路,将常见的业务封装为模版,(比如对单一表单进行的常规列表和表单操作,也就是最常见的CRUD操作。或者一对多表单,列表展示主表数据,点开一条件主表数据,对话框显示主表数据及子表列表,对子表列表进行操作等),只需要动态渲染不同的地方,那么就能够实现只需要设置几个简单的参数,就能够自动的生成自定义表单出来,这里的不同地方无非就是Object对象(Object就定义了不同的字段,在渲染字段的地方全部替换为新的Object的字段),标题内容等少数不同的地方。
模版的实现思路大致为:根据模版Id找到表单模型相关的所有表单和视图,将关联的所用数据表数据读取到内存中,包括规则、控件、视图行、表单项、表单列等,再对Id进行Map映射(新建一个字典对象,读取所有Guid字段的地方,新建映射,Key存储老的Guid,Value存储新建的Guid),将所有数据Guid字段替换为将建的Guid值,将Object对象相关的数据全部删除,用将的Object字段重新生成数据,不同的字段类型设置默认的样式,再将所有内容存储到数据库。
随着表单引擎的使用,可以定义更多的表单模版,那么表单引擎的功能将越来越丰富也越来越容易使用。
部分核心部分代码(可下载源码查看):
private Dictionary<Guid, Guid> idMapes; private void CalculateId(Guid? oldId) { if (!oldId.HasValue) { return; } if (!idMapes.ContainsKey(oldId.Value)) { if (oldId.Value == Guid.Empty) { idMapes.Add(oldId.Value, Guid.Empty); } else { idMapes.Add(oldId.Value, Guid.NewGuid()); } } } public async Task CreateFormFromTemplate(Guid formId, string applicationCode, string objectNameMap, string descriptionMap, string strExcludeCreateFields, string category, int itemRowColCount = 2) { ...... // 查询数据库数据 spriteForms = await spriteCommonRepository.GetCommonList<SpriteForm>("SpriteForms", queryIdFormWhereModels); spriteViews = await spriteCommonRepository.GetCommonList<SpriteView>("SpriteViews", queryIdViewWhereModels); formControls = await spriteCommonRepository.GetCommonList<Control>("Controls", queryBusinessFormWhereModels); viewControls = await spriteCommonRepository.GetCommonList<Control>("Controls", queryBusinessViewWhereModels); formSpriteRules = await spriteCommonRepository.GetCommonList<SpriteRule>("SpriteRules", queryBusinessFormWhereModels); viewSpriteRules = await spriteCommonRepository.GetCommonList<SpriteRule>("SpriteRules", queryBusinessViewWhereModels); formRuleActions = await spriteCommonRepository.GetCommonList<RuleAction>("RuleActions", queryBusinessFormWhereModels); // 替换Id foreach (var spriteForm in spriteForms) { CalculateId(spriteForm.Id); CalculateId(spriteForm.Version); spriteForm.Id = idMapes[spriteForm.Id]; spriteForm.Version = idMapes[spriteForm.Version]; spriteForm.ApplicationCode = applicationCode; spriteForm.Name = ReplaceName(dictObjectNames, dictDescriptions, spriteForm.Name); spriteForm.Description = ReplaceName(dictObjectNames, dictDescriptions, spriteForm.Description); spriteForm.Category = category; spriteForm.IsTemplate = false; } foreach (var spriteView in spriteViews) { CalculateId(spriteView.Id); CalculateId(spriteView.Version); spriteView.Id = idMapes[spriteView.Id]; spriteView.Version = idMapes[spriteView.Version]; spriteView.ApplicationCode = applicationCode; spriteView.Name = ReplaceName(dictObjectNames, dictDescriptions, spriteView.Name); spriteView.Description = ReplaceName(dictObjectNames, dictDescriptions, spriteView.Description); spriteView.Category = category; } // 替换Object数据 ...... }
感觉还是没有把这块内容描述得特别清楚,很多设计思想用文字还是有点难表单出来!
自己做这些不知道有没有意义,最近处于半离职状态,很想把这块内容应用到实际业务系统,再深入耕耘下去,但是又不善于推销自己,也有很多无奈,最近为了生活,不得不从头学习QT。
开源地址:https://gitee.com/kuangqifu/sprite
体验地址:http://47.108.141.193:8031(首次加载可能有点慢,用的阿里云最差的服务器)
自定义表单文章地址:https://www.cnblogs.com/spritekuang/
流程引擎文章地址:https://www.cnblogs.com/spritekuang/category/834975.html(采用WWF开发,已过时,已改用Elsa实现,https://www.cnblogs.com/spritekuang/p/14970992.html )
Github地址:https://github.com/kuangqifu/CK.Sprite.Job