流程引擎在业务流转过程中应用比较广泛,bpmn.js能够很方便将BPMN流程图用到你的应用中。定制化的开发,比如自定义工具栏、汉化等等个性化需求在需求研发过程中比较常见,怎么实现呢?有一些经验分享供参考。
文字2500左右,预计5分钟阅读完
一.需求来源
xxx1.3版本,在场景解决方案-方案配置处需要新增流程配置。这一块功能之前在xx运维平台-模板管理-流程模板处已经做过一版了。但是就产品所说,那一版交互性不是很好,对内推广受到严重阻碍,具体体现在:
-
左侧工具栏图标没有按钮说明,且存在多余的操作按钮
-
点击画布内的元素出现的context-pad,出现多余的操作按钮,混淆视听
-
右侧属性操作面板是英文的 总结就是说就想一目了然,满足基本操作,多余的东西全部去掉。对照着新的需求,需要实现的几点可以概括为:
-
自定义左侧工具栏:a、新增全屏【后因时间较紧,未实现】,并行,会签等操作按钮;b、所有操作按钮底下都新增汉字说明
-
自定义context-pad:去除多余的按扭
-
去除默认的右侧属性栏,自定义右侧属性操作面板,样式和交互大改 实现图对比:
图1:图2**:**
二. 左侧工具栏改造
2.1 增加并行和会签元素
根据源码:
github.com/bpmn-io/bpm…
定义的PaletteProvider,在项目里新建customPalette.js,基本上构造函数,注入变量都不需要修改,只需要在getPaletteEntries中新增actions就行。actions类型新增可分为以下几种情况进行处理:
bpmn已经实现的:
github.com/bpmn-io/bpm…
根据节点类型,查找适合的节点元素。如并行属于网关元素,在GATEWAY数组中找到Parallel Gateway,直接添加即可
bpmn未实现:可根据其他actions的格式,增加对应的属性, 属性说明如下:
"global-connect-tool": { // 元素唯一标识,在其他地方以它为准
group: "tools", // 分组名
className: "bpmn-icon-connection-multi", // 样式类名
title: "连线", // 元素悬浮框显示内容
action: { // 操作
click: function (event) { // 点击时调用的事件
globalConnect.toggle(event);
},
dragstart: () => {}, // 开始拖拽时调用的事件
}
},
bpmn已经实现,但是自己想换的:重写PaletteProvider类,覆盖原型上的getPaletteEntries方法
第一种和第三种的区别还在于在导出的时候__init__变量名定义。第一种可以自定义,第三种必须是paletteProvider。
import CustomPalette from "./customPalette";
export default {
__init__: ["paletteProvider"],
paletteProvider: ["type", CustomPalette],
};
注:即便你用bpmn实现了自定义效果,也得先问后端支不支持解析。每一个元素都需要有对应的解析方法。前端自定义,后端也得进行相应拓展
2.2 增加元素底部说明,新增'工具栏'头部
在global.less里修改样式,通过after增加说明,通过before增加头部。通过样式的方式,在提升美观度的同时,也增强了可读性
.djs-palette {
...
.djs-palette-entries {
...
&:before {
position: absolute;
content: '工具栏';
text-align: center;
font-size: 14px;
line-height: 40px;
left: -1px;
width: calc(100% + 2px);
height: 40px;
top: -40px;
color: #000;
border: 1px solid #ccc;
background: #fff;
box-shadow: -1px 0px 8px #aaaaaa;
}
// 给图标加字
.addFont {
position: absolute;
top: 30px;
left: 12px;
font-size: 12px;
}
.bpmn-icon-hand-tool:after {
content: '移动';
.addFont;
}
.bpmn-icon-lasso-tool:after {
content: '选择';
.addFont;
}
...
}
}
// 图标
.bjs-powered-by {
bottom: 0 !important;
}
注:bpmn官网说了不能对图标进行任何改造,也不能隐藏。所以这也只是稍微往下调整了点
三. 自定义context-pad
跟左侧栏差不多,需要自定义ContextPadProvider的getContextPadEntries函数【github.com/bpmn-io/bpm… 根据选中节点的不同,动态设置显示的context-pad。主要是ContextPadProvider原型上createAction函数的改写【方法名可自定义,在调用时注意统一就行】。
function appendAction(type, className, title, options) {
function appendStart(event, element) {
var shape = elementFactory.createShape(assign({ type: type }, options));
create.start(event, shape, {
source: element
});
}
var append = autoPlace
? function (event, element) {
var shape = elementFactory.createShape(
assign({ type: type }, options)
);
autoPlace.append(element, shape);
}
: appendStart;
return {
group: "model",
className: className,
title: title,
action: {
dragstart: appendStart,
click: append,
}
};
}
var actions = {};
if (
element.type === "bpmn:UserTask" ||
element.type === "bpmn:ServiceTask" ||
element.type === "bpmn:ScriptTask" ||
element.type === "bpmn:StartEvent" ||
element.type === "bpmn:ExclusiveGateway" ||
element.type === "bpmn:ParallelGateway" ||
element.type === "bpmn:ComplexGateway"
) {
actions = {
"append.user-task": appendAction(
"bpmn:UserTask",
"bpmn-icon-user-task",
"节点"
),
...
connect: {
group: "edit",
className: "bpmn-icon-connection-multi",
title: "连线",
action: {
click: startConnect,
dragstart: startConnect
}
}
};
}
assign(actions, {
delete: {
group: "edit",
className: "bpmn-icon-trash",
title: translate("Remove"),
action: {
click: removeElement
}
}
});
return actions;
}
导出customModeler
import CustomPalette from "./customPalette";
import CustomContextPadProvider from "./customContextPadProvider";
export default {
__init__: ["paletteProvider", "contextPadProvider"],
paletteProvider: ["type", CustomPalette],
contextPadProvider: ["type", CustomContextPadProvider]
};
四. 定义Modeler
要用改写过的ContextPadProvider,PaletteProvider类,customModeler需要继承bpmn核心类,这样就可以在具体页面进行调用
import inherits from "inherits";
import Modeler from "bpmn-js/lib/Modeler";
import CustomModule from "./custom";
function CustomModeler(options) {
Modeler.call(this, options);
this._customElements = [];
}
inherits(CustomModeler, Modeler);
CustomModeler.prototype._modules = [].concat(CustomModeler.prototype._modules, [
CustomModule
]);
export { CustomModeler };
在页面里建模调用如下:
function createModel() {
const viewer = new CustomModeler({
container: '#canvas',
propertiesPanel: {
},
...
}
五. 右侧属性栏
bpmn其实提供了一些表单组件来进行搭配操作,考虑到需求上表单项比较少【是否回退,通过条件、任务选择、名称、ID【采用默认】】,所以选用antd4的表单,通过bpmn的操作方法,在表单保存时,实时操作bpmn展示,比上一版增强了联动性,提升用户体验。实现思路:
- 定义变量currentElement, 存储当前操作的元素,可能会存在框选信息,selectedElements用来存储被选中区域里的元素信息
- 初始化右侧面板,达成选中元素变化,则右侧工具栏显示信息联动变化效果
// 初始化右侧属性面板
function initPropertiesPanel() {
bpmnModeler.on('selection.changed', e => {
// 如果是第一个节点,则oldSelection有,newSelection没有
let selection = e.newSelection;
if (!e.newSelection.length && e.oldSelection.length) {
selection = e.oldSelection;
}
setSelectedElements(selection);
setCurrentElement(selection[0])
})
bpmnModeler.on('element.changed', e => {
const { element } = e
if (!currentElement) {
return
}
// 如果选择的节点变化,则更新现在的节点以便更新属性面板
if (element.id === currentElement.id) {
setCurrentElement(element)
}
})
}
- 在表单提交回调函数种,调用updateProperties函数将右侧属性更新到xml上【可直接写入bpmn的属性:名称等属性】
function updateProperties(type, value) {
let properties = {}
properties[type] = value
const modeling = bpmnModeler.get('modeling')
modeling.updateProperties(currentElement, properties)
currentElement[type] = value
setCurrentElement(currentElement)
}
- 通过条件是加在非开始节点连出的线上,可以通过新增节点,实现区分显示【加了通过条件的,会在线上出现菱形】。在还不知道操作方法的时候,在最后提交时强行通过正则匹配将bpmn形成的xml文件插入判断条件,后面使用内部方法,简化了这部分操作。如下:
// 添加新的节点:通过条件在xml上显示为新的节点
function updateCondictionProperties(condition) {
const modeling = bpmnModeler.get('modeling');
const model = bpmnModeler.get('moddle');
const newCondition = model.create('bpmn:FormalExpression', {
body: condition
});
modeling.updateProperties(currentElement, {
conditionExpression: newCondition
})
currentElement.conditionExpression = newCondition
setCurrentElement(currentElement)
}
- 是否回退,任务节点等自定义属性,bpmn提供了自定义属性,但因为后端解析器未支持,会直接报错。前端需要自己存储,在回显的时候,进行匹配显示。通过条件因为bpmn里存储的属性名与页面显示的属性名不对应,也需要做处理。在建模完成时,会绑定操作【bindEvent】,匹配显示逻辑写在element.click的回调里
function bindEvent() {
const eventBus = bpmnModeler.get('eventBus');
const events = [
'element.hover',
'element.out',
'element.click',
'element.dblclick',
'element.mousedown',
'element.mouseup',
];
events.forEach(event => {
eventBus.on(event, e => {
event === 'element.click' && add(e);
});
});
function add(e) {
const {
element: {id, businessObject: { name, $type, sourceRef, conditionExpression }},
} = e;
// 找到表单中对应项
const clickElement = flowNode.find(el => el.flowNodeKey === id);
let otherOptions = {
flowNodeName: name
};
// 设置节点条件
if (clickElement) {
otherOptions = clickElement;
otherOptions.nodeTask = clickElement?.taskFlowRelationReqDtoList[0]?.taskTemplateId || '';
}
// 设置通过条件
if (conditionExpression) {
otherOptions.passCondition = conditionExpression.body;
}
// 记录类型
setNodeType($type);
// 节点的name也是节点,但是不需要显示节点设置弹窗
setIsLabel(id.indexOf('_label') >= 0);
// 从开始节点出来的线不需要设置通过条件
setIsFirstLine((sourceRef?.$type || '') === "bpmn:StartEvent")
form.setFieldsValue({
// 判断id,除节点外的不能显示。新增会有process,编辑的时候是flowDefKey
flowNodeKey: ((id.indexOf('Process')) >= 0) || ((id.indexOf(baseInfo.flowDefKey)) >= 0) ? '' : id,
flowNodeName: otherOptions.flowNodeName || undefined,
isReturnable: otherOptions.isReturnable,
passCondition: otherOptions.passCondition || undefined,
nodeTask: otherOptions.nodeTask || undefined
})
}
}
- 替换processId
在上一版中,提供了输入框改写processId【个人认为交互也不是很好,proccessId为选中跳转进来的流程ID,原则上是默认且不被用户察觉的】。这一版因没有这个信息的输入项,同时也不存在子流程。所以选择在提交时,直接用正则替换掉processid,同时增加isExecutable属性
function saveXML() {
bpmnModeler.saveXML({format: true}, (err, xml) => {
// 流程id和isExecutable需要直接写成processId和true
xml = xml.replace('<bpmn:process id="Process_1" isExecutable="false">',
`<bpmn:process id="${baseInfo.flowDefKey}" isExecutable="true">`)
setXmlContent(xml);
...
});
}
六. 汉化
汉化主要是为了处理context-pad元素的悬浮框显示文字,增强可读性
1.定义汉化对应文件translationsGerman.js
2.汉化处理函数customTranslate
import translations from './translationsGerman';
export default function customTranslate(template, replacements) {
replacements = replacements || {};
template = translations[template] || template;
return template.replace(/{([^}]+)}/g, function(_, key) {
var str=replacements[key];
if(translations[replacements[key]]!=null&&translations[replacements[key]]!='undefined'){
str=translations[replacements[key]];
}
return str || '{' + key + '}';
});
}
- 作为CustomModeler函数additionalModules进行传入
// 汉化
const customTranslateModule = {
translate: ['value', customTranslate]
};
// 建模
function createModel() {
const viewer = new CustomModeler({
container: '#canvas',
propertiesPanel: {
},
additionalModules: [
customTranslateModule
],
moddleExtensions: {
camunda: camundaModdleDescriptor,
},
});
setBpmnModeler(viewer);
}
七. 总结
交互是一大重点,用户体验不好,后期必然会被替换或重构。时间有限,交互还有很多可以改进的地方。共勉~