Activiti动态表单开发技术分享

1. 动态表单特点

一般而言,工作流引擎常用表单有三种:普通表单、外置表单和动态表单。各自都有其优缺点,可根据具体场景灵活选用。需要说明的是,三种表单方式只是在任务节点上用户的表单定义方式上面有差别,而流程的运转机制则完全相同。


image.png

如图所示,区别于普通表单和外置表单,动态表单是直接将工作流节点处的表单嵌入流程定义文件BPMN中,系统利用JS或者模板引擎根据流程定义中表单定义的各个子控件及其属性动态渲染出表单加载出来。

2. 动态表单流程设计

在模板管理界面,点击新增模板按钮,进入流程模板设计页面。

image.png

编辑流程信息:流程key、流程name等:


image.png

拖拽添加启动节点,点击启动节点,在下面的属性中点击动态表单属性:


image.png

编辑启动节点的动态表单属性,编辑活动编号、名称、类型、必输、可读、可写等属性后,点击保存:
image.png

添加下一个活动事件,并编辑该节点属性信息,重点是代理、和动态表单属性信息:


image.png

点击代理,编辑该事件的代理人、候选人(组),点击保存:
image.png

中间流程设计不详细讲述,最后添加一个结束节点并连接:
image.png

动态表单节点的常用属性介绍:

TIM截图20180705173841.png

TIM截图20180705174046.png

流程全部设计完成后,点击保存按钮进行保存:

image.png

在流程玩法-流程列表界面点击部署流程按钮:

image.png

3. 流程列表

流程部署成功后,在待启动流程列表界面可以看到已部署的流程:

image.png

点击启动按钮,弹出启动节点的动态表单,输入信息后点击启动流程按钮:

image.png

4. 任务列表

启动成功后,登录流程设计的该节点候选人(组)用户登录,在任务列表界面可以看到该流程,并可以点击签收按钮进行签收:

image.png

签收成功后,该项操作会变成“办理”状态,可以点击进行办理:

image.png

点击办理按钮,弹出该节点定义的动态表单,并进行提交操作:

image.png

5. 运行中流程

点击运行中流程菜单可以查看已启动但未结束的流程列表,并且可以查看每个流程正在运行的节点:

image.png

点击当前节点,可以查看每个流程图及当前运行节点位置:

image.png

6. 已结束流程

在已结束流程页面可以看到已经结束的流程列表:

image.png

7. 动态表单开发关键点

标准流程的启动和运转直接调用Activiti的通用API即可实现,下面主要从以下几个方面讲解。

1) 动态表单渲染

动态表单将表单定义在了流程定义的文件中,因此在启动节点和任务节点处能分别通过流程定义ID和任务ID去获取节点处的表单属性JSON,如下所示:

获取启动节点处表单数据链接:

<u>http://localhost:8083/form/dynamic/get-form/start/leave-dynamic-from:2:47515</u>

获取表单定义数据结果:

*{*

*"form":{*

*"deploymentId":"47512",*

*"formKey":"",*

*"formProperties":[*

*{*

*"id":"startDate",*

*"name":"请假开始日期",*

*"readable":true,*

*"required":true,*

*"type":{*

*"name":"date"*

*},*

*"value":"",*

*"writable":true*

*},*

*{*

*"id":"endDate",*

*"name":"请假结束日期",*

*"readable":true,*

*"required":true,*

*"type":{*

*"name":"date"*

*},*

*"value":"",*

*"writable":true*

*},*

*{*

*"id":"reason",*

*"name":"请假原因",*

*"readable":true,*

*"required":true,*

*"type":{*

*"mimeType":"text/plain",*

*"name":"string"*

*},*

*"value":"",*

*"writable":true*

*}*

*],*

*"processDefinition":""*

*}*

*}*

获取到了表单定义属性文件后,就可以利用JS或者模板引擎渲染出表单了。比如利用layui的模板引擎来渲染,就可以定义如下模板:

***var****getTpl =* *`*

*<form class="layui-form" lay-filter="form-tpl" style="padding: 10px;">*

*{{# $.each(d.taskFormData.formProperties, function(i,v1) { }} {{# console.log(i,v1)}}*

*<div>*

*{{# if(v1.type.name=="string" ){ }}*

*<div class="layui-form-item">*

*<label class="layui-form-label">{{v1.name}}</label>*

*<div class="layui-input-block">*

*<input class="layui-input" type="text" name="{{v1.writable ? 'fp_'+v1.id : v1.id}}" autocomplete="off" {{v1.required? 'lay-verify="required"': ''}} placeholder="{{v1.name}}" lay-blur lay-verType="tips" value="{{v1.value}}" {{v1.writable ? '':'disabled'}}/>*

*</div>*

*</div>*

*{{# } }}*

*<div></div>*

*{{# if(v1.type.name=="date" ){ }}*

*<div class="layui-form-item">*

*<label class="layui-form-label">{{v1.name}}</label>*

*<div class="layui-input-block">*

*<input class="layui-input date" type="text" name="{{v1.writable ? 'fp_'+v1.id : v1.id}}" autocomplete="off" {{v1.required? 'lay-verify="required|date"': ''}} placeholder="yyyy-MM-dd" lay-verType="tips" value="{{v1.value}}" {{v1.writable ? '':'disabled'}}/>*

*</div>*

*</div>*

*{{# } }}*

*<div></div>*

*{{# if(v1.type.name=="enum" ){ }}*

*<div class="layui-form-item">*

*<label class="layui-form-label">{{v1.name}}</label>*

*<div class="layui-input-block">*

*<select name="{{v1.writable ? 'fp_'+v1.id : v1.id}}" {{v1.writable ? '':'disabled'}}>*

*<option value=""></option>*

*{{# $.each(d[v1.id+''],function(i2,v2){ }}*

*<option value="{{i2}}" {{i2==v1.value ? 'selected':''}}>{{v2}}</option>*

*{{# }) }}*

*</select>*

*</div>*

*</div>*

*{{# } }}*

*</div>*

*{{# }) }}*

*<div style="display:none;">*

*<!--此处隐藏但不能省略,为触发事件准备-->*

*<button lay-submit>提交</button><button type="reset">重置</button>*

*</div>*

*</form>*

*<style type="text/css">*

*.layui-layer-page .layui-layer-content {*

*overflow: visible;*

*}*

*</style>*

*`**;*

上述模板对常见的string、date、enum类型进行了解析和渲染,有更多类型可以自己根据需要添加。

此外,需要注意的是,要区别开表单中可编辑参数与不可编辑参数的属性配置,如下所示(红色标注的部分),可编辑参数的name属性值前面加上“fp_”(约定)。这样配置的好处是后台在接收到参数后可以区分开哪些参数是当前节点需要保存的参数信息,以便进行保存(见下面第二点表单的参数解析部分)。

*<input class="layui-input" type="text"* *name="{{v1.writable ? 'fp_'+v1.id : v1.id}}"** autocomplete="off" {{v1.required? 'lay-verify="required"': ''}} placeholder="{{v1.name}}" lay-blur lay-verType="tips" value="{{v1.value}}" {{v1.writable ? '':'disabled'}}/>*

使用方法是在需要进行动态表单渲染的页面JS中引入该模板:

//引入动态表单渲染模板

$.use(ctx+'/static/js/dynamic-form-common.js', **function****(){});

然后在获得表单属性数据后,调用renderForm方法,传入data数据和需要渲染的页面dom节点元素即可:

renderForm(JSON.parse(form), $form.get(0));

2) 表单参数解析

前文已经提到,在表单渲染时就已经通过设置不同的name属性值来区分开了可编辑参数和不可编辑参数,因此在后台进行参数解析时就能很方便地对可编辑参数进行提取:

// 从request中读取参数然后转换

Map<String, String[]> parameterMap= request.getParameterMap();

Set<Entry<String, String[]>> entrySet= parameterMap.entrySet();

for****(Entry<String, String[]> entry: entrySet) {

String key= entry.getKey();

// fp_的意思是form <u>paremeter</u>

if**** (StringUtils.defaultString(key).startsWith("fp_")) { formProperties.put(key.replaceFirst("fp_", ""), entry.getValue()[0]);
*}
}

上述代码就能将可编辑参数封装在formProperties这个HashMap中。

3) 动态表单自定义类型

下面以上传附件为例,讲述自定义表单类型的步骤和流程:

a. 定义表单扩展类型


***public******class****FileFormType* ***extends**** AbstractFormType {*

*/***

* **

* */*

***private******static******final******long******serialVersionUID**** = 1L;*

*@Override*

***public**** String getName() {*

*//* ***TODO**** Auto-generated method stub*

***return****"file"**;*

*}*

*@Override*

***public****Object convertFormValueToModelValue(String* *propertyValue**) {*

*//* ***TODO**** Auto-generated method stub*

***return****propertyValue**;*

*}*

*@Override*

***public****String convertModelValueToFormValue(Object* *modelValue**) {*

*//* ***TODO**** Auto-generated method stub*

***return**** (String)**modelValue**;*

*}*

*}*

b. 在activiti配置类中注册表单扩展类型

*//注册自定义表单类型*

*List<AbstractFormType>* *formTypes**=* ***new**** ArrayList<>();*

*formTypes**.add(****new**** FileFormType());    **processEngineConfiguration**.setCustomFormTypes(**formTypes**);*

c. 针对表单自定义类型新增动态渲染解析器

*{{# if(v1.type.name=="file" ){ }}*

*<div class="layui-form-item">*

*<label class="layui-form-label">{{v1.name}}</label>*

*<div>*

*<button type="button" class="layui-btn layui-btn-normal" id="test8">选择文件</button>*

*</div>*

*</div>*

*{{# } }}*

d. 任务办理时判断是否包含附件类型,如果包含则应将表单类型设置为“multipart/form-data”:

***if****($(**"#test8"**)){*

*     $form.attr(**'enctype'**,**'multipart/form-data'**);*

*}*

e. 在任务办理时新增附件的保存逻辑:

首先接收参数应新增:

@RequestParam(value="file", required=false) MultipartFile file

在参数解析后,保存file:

**if**(**null**!= file) {

attachmentService.createAttachment(file, taskId, processInstanceId, formProperties.get("attachmentDescription"), request);        

}

4) 常用API总结

引擎API是与Activiti打交道的最常用方式。 从ProcessEngine中,你可以获得很多囊括工作流/BPM方法的服务。 ProcessEngine和服务类都是线程安全的。 你可以在整个服务器中仅保持它们的一个引用就可以了。

ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();    
RuntimeService runtimeService = processEngine.getRuntimeService();  
RepositoryService repositoryService = processEngine.getRepositoryService();  
TaskService taskService = processEngine.getTaskService();  
ManagementService managementService = processEngine.getManagementService();  
IdentityService identityService = processEngine.getIdentityService();  
HistoryService historyService = processEngine.getHistoryService();  
FormService formService = processEngine.getFormService();

① ProcessEngines:

ProcessEngines.getDefaultProcessEngine()会在第一次调用时 初始化并创建一个流程引擎,以后再调用就会返回相同的流程引擎。 使用对应的方法可以创建和关闭所有流程引擎:ProcessEngines.init()和 ProcessEngines.destroy()。

ProcessEngines会扫描所有activiti.cfg.xml和 activiti-context.xml 文件。 对于activiti.cfg.xml文件,流程引擎会使用Activiti的经典方式构建: ProcessEngineConfiguration.createProcessEngineConfigurationFromInputStream(inputStream).buildProcessEngine(). 对于activiti-context.xml文件,流程引擎会使用Spring方法构建:先创建一个Spring的环境, 然后通过环境获得流程引擎。

所有服务都是无状态的。这意味着可以在多节点集群环境下运行Activiti,每个节点都指向同一个数据库, 不用担心哪个机器实际执行前端的调用。 无论在哪里执行服务都没有问题。

② RepositoryService******:

RepositoryService可能是使用Activiti引擎时最先接触的服务。 它提供了管理和控制发布包和流程定义的操作。 这里不涉及太多细节,流程定义是BPMN 2.0流程的java实现。 它包含了一个流程每个环节的结构和行为。 发布包是Activiti引擎的打包单位。一个发布包可以包含多个BPMN 2.0 xml文件和其他资源。 开发者可以自由选择把任意资源包含到发布包中。 既可以把一个单独的BPMN 2.0 xml文件放到发布包里,也可以把整个流程和相关资源都放在一起。 (比如,'hr-processes'实例可以包含hr流程相关的任何资源)。 可以通过RepositoryService来部署这种发布包。 发布一个发布包,意味着把它上传到引擎中,所有流程都会在保存进数据库之前分析解析好。 从这点来说,系统知道这个发布包的存在,发布包中包含的流程就已经可以启动了。

除此之外,服务可以

ü 查询引擎中的发布包和流程定义。

ü 暂停或激活发布包,对应全部和特定流程定义。 暂停意味着它们不能再执行任何操作了,激活是对应的反向操作。

ü 获得多种资源,像是包含在发布包里的文件, 或引擎自动生成的流程图。

ü 获得流程定义的pojo版本, 可以用来通过java解析流程,而不必通过xml。

③ RuntimeService******:

正如RepositoryService负责静态信息(比如,不会改变的数据,至少是不怎么改变的), RuntimeService正好是完全相反的。它负责启动一个流程定义的新实例。 如上所述,流程定义定义了流程各个节点的结构和行为。 流程实例就是这样一个流程定义的实例。对每个流程定义来说,同一时间会有很多实例在执行。 RuntimeService也可以用来获取和保存流程变量。 这些数据是特定于某个流程实例的,并会被很多流程中的节点使用 (比如,一个排他网关常常使用流程变量来决定选择哪条路径继续流程)。 Runtimeservice也能查询流程实例和执行。 执行对应BPMN 2.0中的'token'。基本上执行指向流程实例当前在哪里。 最后,RuntimeService可以在流程实例等待外部触发时使用,这时可以用来继续流程实例。 流程实例可以有很多暂停状态,而服务提供了多种方法来'触发'实例, 接受外部触发后,流程实例就会继续向下执行。

④ TaskService******:

任务是由系统中真实人员执行的,它是Activiti这类BPMN引擎的核心功能之一。 所有与任务有关的功能都包含在TaskService中:

ü 查询分配给用户或组的任务

ü 创建独立运行任务。这些任务与流程实例无关。

ü 手工设置任务的执行者,或者这些用户通过何种方式与任务关联。

ü 认领并完成一个任务。认领意味着一个人期望成为任务的执行者, 即这个用户会完成这个任务。完成意味着“做这个任务要求的事情”。 通常来说会有很多种处理形式。

⑤ IdentityService******:

IdentityService非常简单。它可以管理(创建,更新,删除,查询...)群组和用户。 请注意, Activiti执行时并没有对用户进行检查。 例如,任务可以分配给任何人,但是引擎不会校验系统中是否存在这个用户。 这是Activiti引擎也可以使用外部服务,比如ldap,活动目录,等等。

⑥ FormService******:

FormService是一个可选服务。即使不使用它,Activiti也可以完美运行, 不会损失任何功能。这个服务提供了启动表单任务表单两个概念。 启动表单会在流程实例启动之前展示给用户, 任务表单会在用户完成任务时展示。Activiti支持在BPMN 2.0流程定义中设置这些表单。 这个服务以一种简单的方式将数据暴露出来。再次重申,它时可选的, 表单也不一定要嵌入到流程定义中。

⑦ HistoryService******:

HistoryService提供了Activiti引擎手机的所有历史数据。 在执行流程时,引擎会保存很多数据(根据配置),比如流程实例启动时间,任务的参与者, 完成任务的时间,每个流程实例的执行路径,等等。 这个服务主要通过查询功能来获得这些数据。

⑧ ManagementService******:

ManagementService在使用Activiti的定制环境中基本上不会用到。 它可以查询数据库的表和表的元数据。另外,它提供了查询和管理异步操作的功能。 Activiti的异步操作用途很多,比如定时器,异步操作, 延迟暂停、激活,等等。后续,会讨论这些功能的更多细节。

以下总结下在开发工作流引擎动态表单相关功能时用到的一些API:

查询流程列表:

ProcessDefinitionQuery dynamicQuery= repositoryService.createProcessDefinitionQuery()

.orderByDeploymentId().desc();

启动流程:

identityService.setAuthenticatedUserId(user.getId());

processInstance= formService.submitStartFormData(processDefinitionId, formProperties);

读取启动节点表单数据:

StartFormDataImpl<u>startFormData</u>* = (StartFormDataImpl)* formService.getStartFormData(processDefinitionId);

任务列表查询:

TaskQuery taskQuery= taskService.createTaskQuery()

.taskCandidateOrAssigned(user== null****? "kafeitu":user.getId())

.active().orderByTaskCreateTime().desc();

签收任务:

taskService.claim(taskId, userId);

办理任务:

identityService.setAuthenticatedUserId(user.getId());

formService.submitTaskFormData(taskId, formProperties);

读取Task表单数据:

TaskFormDataImpltaskFormData* = (TaskFormDataImpl)formService.getTaskFormData(taskId*);

运行中流程列表查询:

ProcessInstanceQuery dynamicQuery= runtimeService.createProcessInstanceQuery()

.orderByProcessInstanceId().desc();

已结束流程列表查询:

HistoricProcessInstanceQuery dynamicQuery= historyService.createHistoricProcessInstanceQuery().finished().orderByProcessInstanceEndTime().desc();

挂起流程:

repositoryService.suspendProcessDefinitionById(processDefinitionId, isCascade, **new**** Date());

激活流程:

repositoryService.activateProcessDefinitionById(processDefinitionId, isCascade, **new**** Date());

删除流程:

repositoryService.deleteDeployment(deploymentId, isCascade);

挂起流程实例:

runtimeService.suspendProcessInstanceById(processInstanceId);

激活流程实例:

runtimeService.activateProcessInstanceById(processInstanceId);

删除流程实例:

runtimeService.deleteProcessInstance(processInstanceId, deleteReason);

关于Activiti的更多详细介绍,请参考以下资料:

网址:http://www.mossle.com/docs/activiti/index.html#apiEngine

书籍:Activiti实战

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容