Activiti动态表单开发技术分享
1. 动态表单特点
一般而言,工作流引擎常用表单有三种:普通表单、外置表单和动态表单。各自都有其优缺点,可根据具体场景灵活选用。需要说明的是,三种表单方式只是在任务节点上用户的表单定义方式上面有差别,而流程的运转机制则完全相同。
image.png
如图所示,区别于普通表单和外置表单,动态表单是直接将工作流节点处的表单嵌入流程定义文件BPMN中,系统利用JS或者模板引擎根据流程定义中表单定义的各个子控件及其属性动态渲染出表单加载出来。
2. 动态表单流程设计
在模板管理界面,点击新增模板按钮,进入流程模板设计页面。
image.png编辑流程信息:流程key、流程name等:
image.png
拖拽添加启动节点,点击启动节点,在下面的属性中点击动态表单属性:
image.png
编辑启动节点的动态表单属性,编辑活动编号、名称、类型、必输、可读、可写等属性后,点击保存:
image.png
添加下一个活动事件,并编辑该节点属性信息,重点是代理、和动态表单属性信息:
image.png
点击代理,编辑该事件的代理人、候选人(组),点击保存:
image.png
中间流程设计不详细讲述,最后添加一个结束节点并连接:
image.png
动态表单节点的常用属性介绍:
TIM截图20180705174046.png
流程全部设计完成后,点击保存按钮进行保存:
image.png在流程玩法-流程列表界面点击部署流程按钮:
image.png3. 流程列表
流程部署成功后,在待启动流程列表界面可以看到已部署的流程:
image.png点击启动按钮,弹出启动节点的动态表单,输入信息后点击启动流程按钮:
image.png4. 任务列表
启动成功后,登录流程设计的该节点候选人(组)用户登录,在任务列表界面可以看到该流程,并可以点击签收按钮进行签收:
image.png签收成功后,该项操作会变成“办理”状态,可以点击进行办理:
image.png点击办理按钮,弹出该节点定义的动态表单,并进行提交操作:
image.png5. 运行中流程
点击运行中流程菜单可以查看已启动但未结束的流程列表,并且可以查看每个流程正在运行的节点:
image.png点击当前节点,可以查看每个流程图及当前运行节点位置:
image.png6. 已结束流程
在已结束流程页面可以看到已经结束的流程列表:
image.png7. 动态表单开发关键点
标准流程的启动和运转直接调用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实战