好久没更新文章了,没更新文章的这些时间我都在闭关修炼,努力提升自身技术,毕竟我2020年的flag是成为大牛。
今天给大家带来的这篇文章是整理我使用bpmn-js实现activiti流程设计器的经验之谈,bpmn-js的中文文档不多,很多人都不如何入手开发,并且bpmn-js的后端使用的是Camunda,如何使用activiti也是困扰了很多开发。
问题
需要前后端分离、觉得activiti的设计器不好用、使用bpmn-js实现设计器,但后端用的是activiti,xml不兼容怎么办?
解决
不止我这一种解决方法,我这里只提供我的解决方法。
1 使用Bpmn-js开发设计器
关于bpmn-js如何使用建议搭建去github上面搜索,这里贴上官网地址: github.com/bpmn-io/bpm…
官网案例地址:github.com/bpmn-io/bpm…
笔者开发设计器时参考了霖呆呆的关于bpmn-js从0开发的一系列文章,地址: juejin.im/post/684490…
相信大家看完以上我贴的文章,对bpmn-js已经很熟悉了;接下来我来解释一下我的项目:
-
环境:windows10
-
开发工具:vscode、IDEA
-
**技术:**前端:vue+webpack,后端springboot+activiti
1.1 自定义右边属性面板
如图,是我完全自定义的属性面板
部分代码如下:
<template>
<div>
<el-container style="height: 700px">
<el-aside width="80%" style="border: 1px solid #DCDFE6" >
<div ref="canvas" style="width: 100%;height: 100%"></div>
</el-aside>
<el-main style="border: 1px solid #DCDFE6;background-color:#FAFAFA ">
<el-form label-width="auto" size="mini" label-position="top">
<!-- 动态显示属性面板 -->
<component :is= "propsComponent" :element= "element" :key= "key"></component>
</el-form>
</el-main>
</el-container>
</div>
</template>
我是通过propsComponent属性的变化来显示不同事件的属性,比如用户任务的属性、网关的属性
propsComponent属性是通过监听modeler、element来改变值的,代码如下:
addModelerListener() {
// 监听 modeler
const bpmnjs = this.bpmnModeler
const that = this
// 'shape.removed', 'connect.end', 'connect.move'
const events = ['shape.added', 'shape.move.end', 'shape.removed']
events.forEach(function(event) {
that.bpmnModeler.on(event, e => {
var elementRegistry = bpmnjs.get('elementRegistry')
var shape = e.element ? elementRegistry.get(e.element.id) : e.shape
// console.log(shape)
if (event === 'shape.added') {
console.log('新增了shape');
// 展示新增图形的属性
that.key = e.element.id.replace('_label', '');
that.propsComponent = bpmnHelper.getComponentByEleType(shape.type);
that.element = e.element;
} else if (event === 'shape.move.end') {
console.log('移动了shape')
// 展示新增图形的属性
that.key = shape.id;
that.propsComponent = bpmnHelper.getComponentByEleType(shape.type);
that.element = e.shape;
} else if (event === 'shape.removed') {
console.log('删除了shape')
// 展示默认的属性
that.propsComponent = 'CommonProps'
}
})
})
},
addEventBusListener() {
// 监听 element
let that = this
const eventBus = this.bpmnModeler.get('eventBus')
const eventTypes = ['element.click', 'element.changed', 'selection.changed']
eventTypes.forEach(function(eventType) {
eventBus.on(eventType, function(e) {
if (eventType === 'element.changed') {
that.elementChanged(e)
} else if (eventType === 'element.click') {
console.log('点击了element');
if (!e || e.element.type == 'bpmn:Process') {
that.key = '1';
that.propsComponent = 'CommonProps'
that.element = e.element;
} else {
// 展示新增图形的属性
that.key = e.element.id;
that.propsComponent = bpmnHelper.getComponentByEleType(e.element.type);
that.element = e.element;
}
}
})
})
},
由于vue的特殊性,在使用属性组件前,还需要引入组件
components: {
CommonProps,
ProcessProps,
StartEventProps,
EndEventProps,
IntermediateThrowEventProps,
ExclusiveGatewayProps,
ParallelGatewayProps,
InclusiveGatewayProps,
UserTaskProps,
SequenceFlowProps,
CallActivityProps
},
接下来就是实现各个事件属性的页面了。
1.2 适配activiti
由于bpmn-js官方是适配camunda的,所以对activiti存在不兼容的地方,为了让bpmn-js能使用activiti,我们需要在BpmnModeler中扩展activiti 代码如下:
import activitiModdleDescriptor from '../js/activiti.json';
this.bpmnModeler = new BpmnModeler({
container: canvas,
//添加属性面板,添加翻译模块
additionalModules: [
customTranslateModule,
customControlsModule
],
//模块拓展,拓展activiti的描述
moddleExtensions: {
activiti: activitiModdleDescriptor
}
});
关于activiti.json文件,我建议你看自定义元模型示例
1.2.1 关于activiti.json文件怎么配置🌟
{
"name": "Activiti", // 标识是activiti
"uri": "http://activiti.org/bpmn", // 添加activiti的命名空间
"prefix": "activiti", // 属性前缀
"xml": {
"tagAlias": "lowerCase"
},
"associations": [],
"types": [
{
"name": "Process", // <bpmn2:process> 标签
"isAbstract": true,
"extends": [
"bpmn:Process" // 继承自<bpmn2:process>
],
"properties": [ // 这个标签的属性
{
"name": "candidateStarterGroups", // 属性名
"isAttr": true, // 是否是属性
"type": "String" // 属性类型
},
{
"name": "candidateStarterUsers",
"isAttr": true,
"type": "String"
},
{
"name": "versionTag",
"isAttr": true,
"type": "String"
},
{
"name": "historyTimeToLive",
"isAttr": true,
"type": "String"
},
{
"name": "isStartableInTasklist",
"isAttr": true,
"type": "Boolean",
"default": true // 给属性添加默认值,但这个默认值没有写入xml中
},
{
"name":"executionListener", // 监听器属性
"isAbstract": true, // 抽象
"type":"Expression" // 类型是表达式
}
]
},
// 在这里接着加其他节点
],
"emumerations": [ ]}
例子: 我的项目中需要给用户任务添加自定义的属性 nodeType(节点类型)
{ "name": "UserTask", "isAbstract": true, "extends": [ "bpmn:UserTask" ], "properties": [ { "name": "nodeType", "isAttr": true, "type": "String" }, ] }
1.3 关于部分扩展和完全自定义🌟
拿左侧工具栏来说,前端项目:src/edit-modeler/js/customController/CustomPalette.js文件
问:
可以看到我自定义了用户任务和调用活动节点,其他的节点我用bpmn-js自带的;
那如果我不想用bpmn-js自带的怎么办呢?
解答:
src/edit-modeler/js/customController/index.js文件
import CustomContextPad from './CustomContextPad';import CustomPalette from './CustomPalette';export default {__init__: [ 'customContextPad', 'customPalette' ],customContextPad: [ 'type', CustomContextPad ],customPalette: [ 'type', CustomPalette ]};
这里用的是customPalette,如果要完全自定义则换成paletteProvider;
同理:完全自定义contextPad用contextPadProvider,完全自定义属性面板用propertiesProvider
import CustomContextPad from './CustomContextPad';import CustomPalette from './CustomPalette';export default {__init__: [ 'contextPadProvider', 'paletteProvider' ],contextPadProvider: [ 'type', CustomContextPad ],paletteProvider: [ 'type', CustomPalette ]};
1.4 关于属性前缀🌟
问:
我们都知道,bpmn-js生成的xml文件属性前缀都是camunda,那如何换成我们需要的前缀呢?
答:
有两种方法
一种是扩展json文件,例如我们需要activiti前缀就扩展了activiti.json
第二种就是直接修改初始化xml文件,我们打开设计器时会importXML一个空节点的xml,我们需要在这个xml中加。
例如:我需要加一个normal的前缀,生成属性后为:normal:nodeType;我们在xml中加上这句话:xmlns:normal=”flowable.org/bpmn/normal…
<?xml version="1.0" encoding="UTF-8"?><bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL"xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"xmlns:di="http://www.omg.org/spec/DD/20100524/DI"xmlns:normal="http://flowable.org/bpmn/normal"xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd"id="sample-diagram" targetNamespace="http://activiti.org/bpmn"><bpmn2:process id="Process_1" isExecutable="true"></bpmn2:process><bpmndi:BPMNDiagram id="BPMNDiagram_1"><bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1"></bpmndi:BPMNPlane></bpmndi:BPMNDiagram></bpmn2:definitions>
xml中加上后,设计器生成的属性如何加?
其实很简单,我们updateProperties时可以加上前缀,例如:
modeling.updateProperties(element, {'normal:nodeType': 'nodeType'})
1.5 由于属性面板是自定义的,修改了属性面板的属性值,如何同步到xml中;以及我在图形上修改了属性如何同步属性面板🌟
项目是vue架构,那就充分发挥vue的优势: 监听
部分代码如下:
watch: {id (newVal, oldVal) {const bpmnModeler = this.bpmnModeler();const modeling = bpmnModeler.get('modeling');modeling.updateProperties(this.element,{'id':newVal});},name(newVal, oldVal){const bpmnModeler = this.bpmnModeler();const modeling = bpmnModeler.get('modeling');modeling.updateProperties(this.element,{'name':newVal});},// 监控element值,当发生改变时获取响应的属性element: {deep: true,immediate: true,handler(newVal, oldVal) {if(newVal) {const bpmnModeler = this.bpmnModeler(); // 我这里由于项目原因用的是方法获取bpmnModelerthis.id = newVal.businessObject.get('id');this.name = newVal.businessObject.get('name');// 初始化赋值const modeling = bpmnModeler.get('modeling');modeling.updateProperties(this.element,{'name':this.name});modeling.updateProperties(this.element,{'process_namespace':this.process_namespace});modeling.updateProperties(this.element,{'process_id':this.id});}}}}
由于element是一个复杂的类型,所以深度监听一定要打开。
同步xml:用的是modeling.updateProperties方法,也可以使用newVal.businessObject.$attrs[‘name’] = this.name修改
修改图形属性同步属性面板:由于深度监听了element,所以修改了图形属性就等于修改了element,所以这里会监听到
1.6 如何添加监听器🌟
可以查看src\edit-modeler\components\CommonProps.vue这个文件
1.7 如何添加自定义的标签🌟
我建议你看:![自定义元模型示例](github.com/bpmn-io/bpm…)
1.8 如何添加多实例🌟
这里提供代码添加的方法,可以直接在图形中点击扳手设置多实例
const moddle = bpmnModeler.get('moddle');loopCharacteristics = moddle.create('bpmn:MultiInstanceLoopCharacteristics');loopCharacteristics['collection'] = 'flow_assignee';loopCharacteristics['elementVariable'] = 'flow_assignee';let completionCondition = elementsHelper.createElement('bpmn:FormalExpression', { body: '${mulitiInstance.completeTask(execution,passResult,mulitiActivityId)}' }, loopCharacteristics, bpmnFactory);loopCharacteristics['completionCondition'] = completionCondition;modeling.updateProperties(element, { loopCharacteristics: loopCharacteristics });
1.9 获取全部节点和根节点🌟
// 获取全部节点,也可以用来获取根节点
bpmnModeler._definitions.rootElements[0]
// 根节点
bpmnModeler.get('canvas').getRootElement()
1.10 如何给节点的同级添加节点🌟
例如:
给SequenceFlow的同级添加了BoundaryEvent,只要获取根节点下的所有节点然后push进入你添加的节点就行了
bpmnModeler._definitions.rootElements[0].flowElements.push(boundaryEvent);
1.11 默认导入的空xml,给标签的id赋动态值报:没有可展示的流程/协作🌟
我的默认空xml如下:
最后生成的xml如下:
可以看到上图中id是以数字开头的, 就是这里导致的😂😂😂
只要开头是字母就没事了,例如:id = `T-${uuidv4()}`;
此处应有掌声👏👏👏
1.12 BpmnViewer流程追踪展示流程图,但是流程图被遮挡🌟
加入下代码可解决
const currentViewbox = this.bpmnViewer.get('canvas').viewbox() const widthWindow = window.outerWidth; const heightWindow = window.outerHeight; const elementMid = { x: widthWindow / 2, y: heightWindow / 2 } this.bpmnViewer.get('canvas').viewbox({ x: elementMid.x - currentViewbox.width / 2, y: elementMid.y - currentViewbox.height / 2, width: currentViewbox.width, height: currentViewbox.height }) const width = document.getElementById('canvas').offsetWidth this.bpmnViewer.get('canvas').zoom(width / this.width)
1.13 xml中有两个相同的属性🌟
如果你在扩展的xxx.json文件中,比如activiti.json文件;你在json文件中配置了用户任务的属性flowable:assignee;这个属性是会被添加在businessObject下,那如果我们要通过businessObject.attrs\[‘flowable:assignee’\]修改,属性会被添加在businessObject.attrs这个下面,所以生成xml时会生成两个
1.14 清理画布
bpmnModeler.clear()
1.15 设置默认流
const newDefaultFlow = elementRegistry.get(element.id).businessObject;
modeling.updateProperties(targetElement, { default: newDefaultFlow });
1.16 主子流程调用
活动节点中的属性flowable:calledElementType=”id”可以是id也可以是key,id表示的是流程定义表中的id, key也就是定义表中的key字段
1.17 禁用一些画布的操作
const bpmnModeler = new BpmnModeler({
container: '#canvas',
additionalModules:[
BpmnModeler, {
paletteProvider:['value',''], // 禁用左面板
labelEditingProvider:['value', ''], // 禁用编辑
contextPadProvider: ['value', ''], // 禁用点击出现的contextPad
bendpoints: [ 'value', {} ], // 禁止流程线变换waypoints
zoomScroll:['value',''],// 禁止画布滚动
moveCanvas:['value',''],// 禁止拖拽
}
],
height: '400px'
});
如果后端传给前端的是json文件,不是xml;请大胆的怼回去🤔️🤔️🤔️
1.18 隐藏bpmnjs图标
.bjs-powered-by {
display:none !important;
}
或者:
// 删除 bpmn logo bpmn.io官方要求不给删或者隐藏,否则侵权 内部使用const bjsIoLogo = document.querySelector('.bjs-powered-by');while (bjsIoLogo.firstChild) { bjsIoLogo.removeChild(bjsIoLogo.firstChild);}
1.19 超时自动完成
xml如下:
<sequenceFlow id="Flow_1hu7yoy" sourceRef="Activity_1ig8oe5" targetRef="Event_1xmxdxy" />
<serviceTask id="Activity_1ig8oe5">
<incoming>Flow_1tvddwv</incoming>
<outgoing>Flow_1hu7yoy</outgoing>
</serviceTask>
<boundaryEvent id="Event_1bi4wq0" attachedToRef="Activity_1ig8oe5">
<timerEventDefinition id="TimerEventDefinition_0wsqmm3" />
</boundaryEvent>
timerEventDefinition里的时间属性添加一下就ok啦
2 后端activiti实现
具体怎么搭建activiti环境,相信大家都能百度到,我只介绍怎么将bpmn-js和activiti兼容
3.1 解析BPMN文件
如图,展示了一个XML格式的流程文件如何经过几个大的步骤部署到引擎的过程
3.2 先由前端传xml保存到后端开始
http请求将携带主要的两个参数,bpmn_xml和svg_xml
由于activiti保存在数据库中的是json文件,所以我们需要将bpmn_xml文件转换成json
activiti官方提供的转换方法并不能满足我,我自定义了转换方法和解析器,activiti官方也允许你自定义解析器
先上方法:
public static JsonNode converterXmlToJson(String bpmnXml) {
// 创建转换对象
BpmnXMLConverter bpmnXMLConverter = new BpmnXMLConverter();
// XMLStreamReader读取XML资源
XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
StringReader stringReader = new StringReader(bpmnXml);
XMLStreamReader xmlStreamReader = null;
try {
xmlStreamReader = xmlInputFactory.createXMLStreamReader(stringReader);
} catch (XMLStreamException e) {
e.printStackTrace();
}
// UserTaskXMLConverter类是我自定义的
BpmnXMLConverter.addConverter(new UserTaskXMLConverter());
// 把xml转换成BpmnModel对象
BpmnModel bpmnModel = bpmnXMLConverter.convertToBpmnModel(xmlStreamReader);
// BpmnJsonConverter类是我自定义的
// 创建转换对象
BpmnJsonConverter bpmnJsonConverter = new BpmnJsonConverter();
// 把BpmnModel对象转换成json
JsonNode jsonNodes = bpmnJsonConverter.convertToJson(bpmnModel);
// 返回的json会被保存到数据库中
return jsonNodes;
}
以上代码使用了Activiti的activiti-bpmn-converter模块提供的BpmnModel对象与XML的互转功能,通过创建org.activiti.bpmn.converter.BpmnXMLConverter类对象调用相应的方法即可实现BpmnModel对象与XML之间的转换操作。
首先,自定义类UserTaskXMLConverter是因为我的用户任务事件中有自定义的属性;在将xml转为BpmnModel时,如果是用户任务事件就会走我自定义的UserTaskXMLConverter类
然后是将BpmnModel转为json,注意每个bpmnModel.attributes下存方着所有属性
3.3 自定义的BpmnJsonConverter文件
Activiti提供的activiti-json-converter模块中提供了BpmnJsonConverter类,我们对比一下我自定义的和官方的
发现,我们自定义的类中的static中有几个Custom开头的类,见名知义,这些类是关于用户任务、流程、网关的转换类。
问:为何要自定义这些类呢?
答:
1. 因为前端自定义属性(例如:多实例属性、默认流程属性)使用官方的toBpmnModel转换是会丢失自定义属性的,我们自定义类主要是将自定义属性放在attribute中,并且转换多实例属性为Activiti的BPMN规范接受。
2. convertElementToJson时加上自定义的属性键值
用户任务自定义属性转换相关代码:
// 多实例类型
String multiInstanceType = getPropertyValueAsString(PROPERTY_MULTIINSTANCE_TYPE, elementNode);
// 通过权重
String multiInstanceCondition = getPropertyValueAsString(PROPERTY_MULTIINSTANCE_CONDITION, elementNode);
if (StringUtils.isNotEmpty(multiInstanceType) && !"none".equalsIgnoreCase(multiInstanceType)) {
String name = getPropertyValueAsString(PROPERTY_NAME, elementNode);
MultiInstanceLoopCharacteristics multiInstanceObject = new MultiInstanceLoopCharacteristics();
if ("sequential".equalsIgnoreCase(multiInstanceType)) {
multiInstanceObject.setSequential(true);
} else {
multiInstanceObject.setSequential(false);
}
if (StringUtils.isNotEmpty(multiInstanceCondition)) {
try {
Integer.valueOf(multiInstanceCondition);
} catch (Exception ex) {
throw new WorkflowApiException(name + "配置成了会签,但通过权重不是一个整数");
}
multiInstanceObject.setCompletionCondition("${nextTaskEvaluator.isComplete(execution," + multiInstanceCondition + ")}");
} else {
throw new WorkflowApiException(name + "配置成了会签,但没有配置通过权重");
}
}
3.4 Bpmn解析处理器
Activiti支持在解析BPMN资源文件时允许自定义BPMN解析处理器(BpmnParseHandler)参与,可以在开始解析一个元素(Element)或解析完之后调用自定义的BPMN解析处理器,在自定义的解析处理器中,我们可以更改一些BPMN对象的属性。
添加BPMN解析处理器可以在Activiti引擎配置文件中配置属性“preBpmnParseHandlers”和“postBpmnParseHandlers”。下面的代码针对Pre(前置)和Post(后置)类型分别添加了一个解析处理器
上面的代码添加了两种类型的BPMN解析处理器,之所以区分类型是为了更细致地划分处理器类型;Pre类型处理器是总是排在第一位执行,也就是在所有流程文件中定义地元素之前,而Post类型的处理器被放在最后执行,也就是所有流程文件中定义的而元素之后。如果解析处理器有特定的顺序要求,就可以用Pre和Post类型来区分。
小结
总体来说,完整开发下来还是比较费力,需要你对bpmn-js以及activiti有一定的了解并且有一定的耐心。
啦啦啦~~ ,写完了写完了,我又是一个开心的小仙女了。
今天的文章流程设计器分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/14790.html