一、bpmn的基本认识
bpmn.js是一个BPMN2.0渲染工具包和web建模器, 使得画流程图的功能在前端来完成。
bpmn画图具有哪些内容?
二、使用npm安装bpmn.js
npm install --save bpmn-js
三、在Angular中使用bpmn.js
1.实现编辑器组件
- 安装相关依赖
npm install --save bpmn-js
- 编写HTML代码
<div class="container">
<div class="canvas"></div>
</div>
- 编写CSS代码
.container{
position: absolute;
height: 100%;
width: 100%;
.canvas{
height: 100%;
width: 100%;
}
}
- 创建mock文件夹,新增xmlStr.ts(默认展示的bpmn流程图)
export var xmlStr:any = `
<?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"
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd" id="sample-diagram"
targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn2:process id="Process_1" isExecutable="false">
<bpmn2:startEvent id="StartEvent_1"/>
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds height="36.0" width="36.0" x="412.0" y="240.0"/>
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>
`
- 编写JS代码
//diagram.component.ts
import { Component } from '@angular/core';
import BpmnModeler from 'bpmn-js/lib/Modeler';
import { xmlStr } from './mock/xmlStr';
@Component({
selector: 'app-diagram',
templateUrl: './diagram.component.html',
styleUrls: ['./diagram.component.scss']
})
export class DiagramComponent {
bpmnModeler:any;
constructor() {
}
ngOnInit(): void {
this.loadBPMN();
}
loadBPMN() {
this.bpmnModeler = new BpmnModeler({
container: '.canvas'
})
this.createNewDiagram(xmlStr);
}
createNewDiagram(xml:any) {
// 将字符串转换成图显示出来
this.bpmnModeler.importXML(xml);
}
}
2.左侧工具栏
3.自定义context-pad
修改方式与palette的修改方式大同小异,现在项目工程下文件夹下新建context-pad文件夹,添加ContextPadProvider.js、index.js文件,参考node-modules中的bpmn-js下面的context-pad文件夹,可以完全拷贝过来。
点击查看ContextPadProvider.js
import { assign, forEach, isArray } from "min-dash";
import { is } from "bpmn-js/lib/util/ModelUtil";
import { isExpanded, isEventSubProcess } from "bpmn-js/lib/util/DiUtil";
import { isAny } from "bpmn-js/lib/features/modeling/util/ModelingUtil";
import { getChildLanes } from "bpmn-js/lib/features/modeling/util/LaneUtil";
import { hasPrimaryModifier } from "diagram-js/lib/util/Mouse";
/**
* A provider for BPMN 2.0 elements context pad
*/
export default function ContextPadProvider(
config,
injector,
eventBus,
contextPad,
modeling,
elementFactory,
connect,
create,
popupMenu,
canvas,
rules,
translate
) {
config = config || {};
contextPad.registerProvider(this);
this._contextPad = contextPad;
this._modeling = modeling;
this._elementFactory = elementFactory;
this._connect = connect;
this._create = create;
this._popupMenu = popupMenu;
this._canvas = canvas;
this._rules = rules;
this._translate = translate;
if (config.autoPlace !== false) {
this._autoPlace = injector.get("autoPlace", false);
}
eventBus.on("create.end", 250, function (event) {
var shape = event.context.shape;
if (!hasPrimaryModifier(event)) {
return;
}
var entries = contextPad.getEntries(shape);
if (entries.replace) {
entries.replace.action.click(event, shape);
}
});
}
ContextPadProvider.$inject = [
"config.contextPad",
"injector",
"eventBus",
"contextPad",
"modeling",
"elementFactory",
"connect",
"create",
"popupMenu",
"canvas",
"rules",
"translate",
];
ContextPadProvider.prototype.getContextPadEntries = function (element) {
var contextPad = this._contextPad,
modeling = this._modeling,
elementFactory = this._elementFactory,
connect = this._connect,
create = this._create,
popupMenu = this._popupMenu,
canvas = this._canvas,
rules = this._rules,
autoPlace = this._autoPlace,
translate = this._translate;
var actions = {};
if (element.type === "label") {
return actions;
}
var businessObject = element.businessObject;
function startConnect(event, element) {
connect.start(event, element);
}
function removeElement(e) {
modeling.removeElements([element]);
}
function getReplaceMenuPosition(element) {
var Y_OFFSET = 5;
var diagramContainer = canvas.getContainer(),
pad = contextPad.getPad(element).html;
var diagramRect = diagramContainer.getBoundingClientRect(),
padRect = pad.getBoundingClientRect();
var top = padRect.top - diagramRect.top;
var left = padRect.left - diagramRect.left;
var pos = {
x: left,
y: top + padRect.height + Y_OFFSET,
};
return pos;
}
/**
* Create an append action
*
* @param {String} type
* @param {String} className
* @param {String} [title]
* @param {Object} [options]
*
* @return {Object} descriptor
*/
function appendAction(type, className, title, options) {
if (typeof title !== "string") {
options = title;
title = translate("Append {type}", { type: type.replace(/^bpmn:/, "") });
}
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,
},
};
}
function splitLaneHandler(count) {
return function (event, element) {
// actual split
modeling.splitLane(element, count);
// refresh context pad after split to
// get rid of split icons
contextPad.open(element, true);
};
}
if (
isAny(businessObject, ["bpmn:Lane", "bpmn:Participant"]) &&
isExpanded(businessObject)
) {
var childLanes = getChildLanes(element);
assign(actions, {
"lane-insert-above": {
group: "lane-insert-above",
className: "bpmn-icon-lane-insert-above",
title: translate("Add Lane above"),
action: {
click: function (event, element) {
modeling.addLane(element, "top");
},
},
},
});
if (childLanes.length < 2) {
if (element.height >= 120) {
assign(actions, {
"lane-divide-two": {
group: "lane-divide",
className: "bpmn-icon-lane-divide-two",
title: translate("Divide into two Lanes"),
action: {
click: splitLaneHandler(2),
},
},
});
}
if (element.height >= 180) {
assign(actions, {
"lane-divide-three": {
group: "lane-divide",
className: "bpmn-icon-lane-divide-three",
title: translate("Divide into three Lanes"),
action: {
click: splitLaneHandler(3),
},
},
});
}
}
assign(actions, {
"lane-insert-below": {
group: "lane-insert-below",
className: "bpmn-icon-lane-insert-below",
title: translate("Add Lane below"),
action: {
click: function (event, element) {
modeling.addLane(element, "bottom");
},
},
},
});
}
if (is(businessObject, "bpmn:FlowNode")) {
if (is(businessObject, "bpmn:EventBasedGateway")) {
assign(actions, {
"append.receive-task": appendAction(
"bpmn:ReceiveTask",
"bpmn-icon-receive-task"
),
"append.message-intermediate-event": appendAction(
"bpmn:IntermediateCatchEvent",
"bpmn-icon-intermediate-event-catch-message",
translate("Append MessageIntermediateCatchEvent"),
{ eventDefinitionType: "bpmn:MessageEventDefinition" }
),
"append.timer-intermediate-event": appendAction(
"bpmn:IntermediateCatchEvent",
"bpmn-icon-intermediate-event-catch-timer",
translate("Append TimerIntermediateCatchEvent"),
{ eventDefinitionType: "bpmn:TimerEventDefinition" }
),
"append.condition-intermediate-event": appendAction(
"bpmn:IntermediateCatchEvent",
"bpmn-icon-intermediate-event-catch-condition",
translate("Append ConditionIntermediateCatchEvent"),
{ eventDefinitionType: "bpmn:ConditionalEventDefinition" }
),
"append.signal-intermediate-event": appendAction(
"bpmn:IntermediateCatchEvent",
"bpmn-icon-intermediate-event-catch-signal",
translate("Append SignalIntermediateCatchEvent"),
{ eventDefinitionType: "bpmn:SignalEventDefinition" }
),
});
} else if (
isEventType(
businessObject,
"bpmn:BoundaryEvent",
"bpmn:CompensateEventDefinition"
)
) {
assign(actions, {
"append.compensation-activity": appendAction(
"bpmn:Task",
"bpmn-icon-task",
translate("Append compensation activity"),
{
isForCompensation: true,
}
),
});
} else if (
!is(businessObject, "bpmn:EndEvent") &&
!businessObject.isForCompensation &&
!isEventType(
businessObject,
"bpmn:IntermediateThrowEvent",
"bpmn:LinkEventDefinition"
) &&
!isEventSubProcess(businessObject)
) {
assign(actions, {
"append.end-event": appendAction(
"bpmn:EndEvent",
"bpmn-icon-end-event-none",
translate("Append EndEvent")
),
"append.append-user-task": appendAction(
"bpmn:UserTask",
"bpmn-icon-user-task",
translate("Append UserTask")
),
"append.gateway": appendAction(
"bpmn:ExclusiveGateway",
"bpmn-icon-gateway-xor",
translate("Append Gateway")
),
// 'append.append-task': appendAction(
// 'bpmn:Task',
// 'bpmn-icon-task',
// translate('Append Task')
// ),
// 'append.intermediate-event': appendAction(
// 'bpmn:IntermediateThrowEvent',
// 'bpmn-icon-intermediate-event-none',
// translate('Append Intermediate/Boundary Event')
// )
});
}
}
if (!popupMenu.isEmpty(element, "bpmn-replace")) {
// Replace menu entry
assign(actions, {
replace: {
group: "edit",
className: "bpmn-icon-screw-wrench",
title: translate("Change type"),
action: {
click: function (event, element) {
var position = assign(getReplaceMenuPosition(element), {
cursor: { x: event.x, y: event.y },
});
popupMenu.open(element, "bpmn-replace", position);
},
},
},
});
}
if (
isAny(businessObject, [
"bpmn:FlowNode",
"bpmn:InteractionNode",
"bpmn:DataObjectReference",
"bpmn:DataStoreReference",
])
) {
// assign(actions, {
// 'append.text-annotation': appendAction('bpmn:TextAnnotation', 'bpmn-icon-text-annotation'),
// 'connect': {
// group: 'connect',
// className: 'bpmn-icon-connection-multi',
// title: translate('Connect using ' +
// (businessObject.isForCompensation ? '' : 'Sequence/MessageFlow or ') +
// 'Association'),
// action: {
// click: startConnect,
// dragstart: startConnect
// }
// }
// });
assign(actions, {
connect: {
group: "connect",
className: "bpmn-icon-connection-multi",
title: translate("Connect using DataInputAssociation"),
action: {
click: startConnect,
dragstart: startConnect,
},
},
});
}
if (
isAny(businessObject, [
"bpmn:DataObjectReference",
"bpmn:DataStoreReference",
])
) {
assign(actions, {
connect: {
group: "connect",
className: "bpmn-icon-connection-multi",
title: translate("Connect using DataInputAssociation"),
action: {
click: startConnect,
dragstart: startConnect,
},
},
});
}
// delete element entry, only show if allowed by rules
var deleteAllowed = rules.allowed("elements.delete", { elements: [element] });
if (isArray(deleteAllowed)) {
// was the element returned as a deletion candidate?
deleteAllowed = deleteAllowed[0] === element;
}
//这里做判断,禁止开始及开始后的userTask的删除
if (deleteAllowed && element.id != "start" && element.id != "apply") {
assign(actions, {
delete: {
group: "edit",
className: "bpmn-icon-trash",
title: translate("Remove"),
action: {
click: removeElement,
},
},
});
}
return actions;
};
function isEventType(eventBo, type, definition) {
var isType = eventBo.$instanceOf(type);
var isDefinition = false;
var definitions = eventBo.eventDefinitions || [];
forEach(definitions, function (def) {
if (def.$type === definition) {
isDefinition = true;
}
});
return isType && isDefinition;
}
点击查看index.js
import DirectEditingModule from 'diagram-js-direct-editing';
import ContextPadModule from 'diagram-js/lib/features/context-pad';
import SelectionModule from 'diagram-js/lib/features/selection';
import ConnectModule from 'diagram-js/lib/features/connect';
import CreateModule from 'diagram-js/lib/features/create';
import PopupMenuModule from 'bpmn-js/lib/features/popup-menu';
import ContextPadProvider from './ContextPadProvider';
export default {
__depends__: [
DirectEditingModule,
ContextPadModule,
SelectionModule,
ConnectModule,
CreateModule,
PopupMenuModule
],
__init__: [ 'contextPadProvider' ],
contextPadProvider: [ 'type', ContextPadProvider ]
};
4.添加bpmn自带属性列表
- 添加bpmn-js-properties-panel组件
npm install bpmn-js-properties-panel --save
- bpmn.js适配的是流程引擎Camunda,所以如果需要更加完整的属性输入框,也需安装camunda-bpmn-moddle,用于camunda数据对象
npm install camunda-bpmn-moddle --save
- 以上安装好之后在angular.json的styles中添加样式
"node_modules/bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css"
5.添加bpmn自定义属性列表
由于扩展使用官方提供的properties-panel,不能满足公司的业务要求。所以,大部分情况下需要自定义bpmn属性栏。
自定义propertiesPanel之前如果引用过原有的属性面板,则要去掉所有关于propertiesPanel引用的js与css,包括原有的属性面板模块propertiesPanelModule、propertiesProviderModule;如果有使用过bpmn原属性面板,则注释以下的引用。

-
bpmn完全自定义右侧属性面板,其实就是完全自己写template组件,自由开发,最大化满足需求,所有与画布的交互全部通过Modeler的实例对象(modeler);跟正常开发页面一样,先定义好自己的属性面板组件,传入Modeler以及其他参数进行使用;
点击查看html
<div class="container">
<div class="header">
<button nz-button nzType="primary" class="headerBtn" (click)="previewXML()">
控制台预览xml
</button>
</div>
<div class="canvas"></div>
<!-- <div id="js-properties-panel" class="panel"></div> -->
<app-custom-properties-panel
[bpmnModeler]="bpmnModeler"
></app-custom-properties-panel>
</div>
点击查看部分js
// 加载bpmn默认流程图
loadBPMN() {
const customTranslateModule = {
translate: ['value', customTranslate],
};
this.bpmnModeler = new BpmnModeler({
container: '.canvas',
// 添加控制板
propertiesPanel: { // new Modeler() 时,必须传入配置项 propertiesPanel,并设置 parent 属性,用来指定侧边栏挂载的 DOM 节点
parent: '#js-properties-panel',
},
additionalModules: [
customTranslateModule, // 汉化模块
paletteProvider, // 自定义palette
contextPadProvider, // 自定义context-pad
// 右边的属性栏
// propertiesProviderModule, // 表示的是属性栏里的内容, 也就是点击不同的element该显示什么内容
// propertiesPanelModule // 表示的是属性栏这个框, 就是告诉别人这里要有个属性栏
],
moddleExtensions: {
// camunda: CamundaModdleDescriptor // bpmn.js适配的是流程引擎Camunda,所以如果需要更加完整的属性输入框,也需安装camunda-bpmn-moddle,用于camunda数据对象
activiti: activitiModdleExtension // 后端需要使用activiti引擎
}
})
// 将字符串转换成图显示出来
this.bpmnModeler.importXML(xmlStr);
}
点击查看部分custom-properties-panel(自定义属性栏)
ngOnInit(): void {
this.validateForm = this.fb.group({
id: [{ value: null, disabled: true }],
name: [null],
category: [null],
skipExpression: ['true'],
documentation: [null],
});
this.loadModuler();
}
// 加载点击元素的属性
loadModuler() {
const { bpmnModeler } = this;
bpmnModeler.on('selection.changed', (e: { newSelection: any[] }) => {
this.element = e.newSelection[e.newSelection.length - 1]; //bpmn-js7+的版本,元素可多选。这里默认为多选最后点击的元素。
if(this.element) {
this.validateForm.patchValue({
id: this.element?.id,
name: this.element?.businessObject?.name,
category: this.element?.businessObject?.category,
skipExpression: this.element?.businessObject?.skipExpression ?? 'true',
documentation: this.element?.businessObject?.documentation?.[0]?.text
});
if(this.validateForm.get('skipExpression')?.value === 'true') {
this.changeField('true', 'skipExpression', 'select');
this.changeShapeBgColor( '#66AA66');
}
this.cd.detectChanges();
}
})
}
-
如何同步修改自定义属性面板和bpmn流程图的值?
其实就是通过,监听输入框的变化,通过modeling.updatePropertie方法进行修改流程图上的属性。
/**
* 输入框改变事件
* @param event
* @param type 字段类型
* @param formControlType 控件类型(可选)
*/
changeField(event: any, type: any, formControlType?: any) {
// if(event || event == null) {
const value = formControlType == 'select' ? (Array.isArray(event) ? event.join() : event ) : (event.target.value ?? '');
let properties: any = {};
properties[type] = value;
this.element[type] = value;
this.updateProperties(properties);
// }
}
// 更新元素属性
updateProperties(properties: any) {
const { bpmnModeler, element } = this;
const modeling = bpmnModeler.get('modeling');
modeling.updateProperties(element, properties);
}
6.bpmn页面汉化
-
通过 https://github.com/bpmn-io/bpmn-js-examples/tree/master/i18n/app ,将 customTranslate文件夹复制到项目文件夹下,然后在app.component.ts中引入。
import customTranslate from 'src/app/diagram/customTranslate/customTranslate'
// 加载bpmn默认流程图
loadBPMN() {
const customTranslateModule = {
translate: ['value', customTranslate],
};
this.bpmnModeler = new BpmnModeler({
container: '.canvas',
// 添加控制板
propertiesPanel: { // new Modeler() 时,必须传入配置项 propertiesPanel,并设置 parent 属性,用来指定侧边栏挂载的 DOM 节点
parent: '#js-properties-panel',
},
additionalModules: [
customTranslateModule, // 汉化模块
paletteProvider, // 自定义palette
contextPadProvider, // 自定义context-pad
// 右边的属性栏
// propertiesProviderModule, // 表示的是属性栏里的内容, 也就是点击不同的element该显示什么内容
// propertiesPanelModule // 表示的是属性栏这个框, 就是告诉别人这里要有个属性栏
],
moddleExtensions: {
// camunda: CamundaModdleDescriptor // bpmn.js适配的是流程引擎Camunda,所以如果需要更加完整的属性输入框,也需安装camunda-bpmn-moddle,用于camunda数据对象
activiti: activitiModdleExtension // 后端需要使用activiti引擎
}
})
// 将字符串转换成图显示出来
this.bpmnModeler.importXML(xmlStr);
}
7.bpmn常用的api
-
流程设计器编辑好的流程图形如何获取xml、svg?
const {xml} = this.modeler.saveXML({format: true})
const {svg} = this.modeler.saveSVG()
-
拿到xml如何渲染成流程图?
this.modeler.importXML(xml)
-
如何让流程图自动居中、流程图缩放?
this.modeler.get('canvas').zoom('fit-viewport', 'auto')//画布自适应居中
this.modeler.get('canvas').zoom(2.0)//放大至2倍
-
获取流程所有图形shape对象
this.elementRegistry.getAll()[0].children
-
新建流程时初始化的xml(xmlStr.ts)
export var xmlStr:any = `
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<process id="Process_start" isExecutable="true" flowable:formDisplay="0">
</process>
<bpmndi:BPMNDiagram id="BpmnDiagram_1">
<bpmndi:BPMNPlane id="BpmnPlane_1" bpmnElement="Process_start">
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
`
-
设置图形shape节点的颜色
this.modeling = this.modeler.get('modeling')
this.modeling.setColor(shapes, { stroke: 'green' })
//shapes可以是单个shape对象,也可以是shape对象数组
-
通过图形id获取图形shape节点对象
this.elementRegistry = this.modeler.get("elementRegistry")
let shape = this.elementRegistry.get(shapeId)
-
改变图形shape节点的某些属性
this.modeling.updateProperties(shape,{
name: '用户任务',
loopCharacteristics: loopCharacteristics,//多实例
extensionElements: extensions,//扩展属性
'flowable:assignee': 'userId_123'//flowable前缀属性
});
-
获取根节点 bpmn:process
this.modeler.getDefinitions().rootElements[0]
-
鼠标选中节点图形事件
this.modeler.on('selection.changed', e => {
const tempElement =e && e.newSelection && e.newSelection[0]
if(tempElement && tempElement.type !="bpmn:Process"){
this.currentElement = tempElement
}
})
-
节点图形属性改变事件
this.modeler.on('element.changed', e => {
if(e.element && e.element.type!="bpmn:Process"){
this.currentElement = e.element
}
})
-
自动选中/取消选中图形事件
//选中
this.modeler.get('selection').select(shapes)
//shapes参数为某个图形shape对象,也可以是图形数组[shape1,shape2,...],代表选中多个图形节点
//注意:此方法会触发this.modeler.on('selection.changed', callback)事件
//取消选中
this.modeler.get('selection').deselect(shape)
//注意:取消选中只能传单个element对象,不支持数组
-
bpmnjs怎么禁止节点拖动和编辑等功能变成只读模式?
-
bpmnjs如何改变element的border及background的颜色?
// 启用改变事件
skipExpressionChange(e: any) {
if (e) {
if (e && e != this.element.businessObject.skipExpression) {
this.changeField(e, 'skipExpression', 'select');
this.changeShapeBgOrBorderColor(e === 'true' ? '#B1ADAD' : '#000000', 'border');
}
}
}
// 改变shape背景或边框颜色值
changeShapeBgOrBorderColor(color: string, type?: string) {
const setting = type === 'background' ? { fill: color } : { stroke: color };
this.element.type == 'bpmn:UserTask' && this.bpmnModeler.get('modeling').setColor(this.element, setting);
}
8.参考资料
https://juejin.cn/post/6844904017567416328
http://www.seozhijia.net/vue/201.html
https://blog.csdn.net/qq_34532969/article/details/107539902
https://www.cnblogs.com/lemoncool/p/12964509.html
https://juejin.cn/post/7117481147277246500
https://juejin.cn/post/6900793894263488519
https://juejin.cn/post/6844904186304266253
https://juejin.cn/post/6912331982701592590
https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU5MDY1MzcyOQ==&action=getalbum&album_id=1576254888626454529&scene=173&from_msgid=2247484449&from_itemidx=1&count=3&nolastread=1#wechat_redirect
来源:https://www.cnblogs.com/chenzilong/p/17048757.html |