Activiti动态表单开发技术分享

1. 动态表单特色

通常而言,工做流引擎经常使用表单有三种:普通表单、外置表单和动态表单。各自都有其优缺点,可根据具体场景灵活选用。须要说明的是,三种表单方式只是在任务节点上用户的表单定义方式上面有差异,而流程的运起色制则彻底相同。 image.pngcss

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

2. 动态表单流程设计

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

image.png

编辑流程信息:流程key、流程name等: image.pngjava

拖拽添加启动节点,点击启动节点,在下面的属性中点击动态表单属性: image.png数据库

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

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

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

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

动态表单节点的经常使用属性介绍: 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的模板引擎来渲染,就能够定义以下模板:

vargetTpl = `

<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实战

相关文章
相关标签/搜索