BPMN.JS
前言
此文章是看过lindandan大佬后,结合自己的总结写的,侵权告知删。
bpmn.js是一个BPMN2.0渲染工具包和web建模器, 使得画流程图的功能在前端来完成.
快速使用
安装:npm i bpmn-js
import BpmnModeler from 'bpmn-js';
import testDiagram from './test-diagram.bpmn'; //导入bpmn文件
var viewer = new BpmnModeler({
container: '#canvas'
});
viewer.importXML(testDiagram, function(err) {
if (!err) {
console.log('success!');
viewer.get('canvas').zoom('fit-viewport'); // 将画布缩放到适应视口的大小
} else {
console.log('something went wrong:', err);
}
});
viewer.importXML(pizzaDiagram).then(function(result) {
console.log('success !', result);
viewer.get('canvas').zoom('fit-viewport');
}).catch(function(err) {
console.log('something went wrong:', err);
});
自定义palette
创建CustomPalette.js
文件
使用类的方式来自定义一个'工具箱'
export default class CustomPalette {
constructor(bpmnFactory, create, elementFactory, palette, translate) {
this.bpmnFactory = bpmnFactory; //用于获取、创建 bpmn内置的节点
this.create = create; //用于将节点渲染到页面的相关操作函数
this.elementFactory = elementFactory; //用于创建元素,如矩形、椭圆的节点
this.translate = translate; //用于翻译模板字符串,通过内置的字典,配合 `xx{字典}`格式的字符串来插值替换的函数
palette.registerProvider(this); //指定这是一个palette
}
// 内置的方法,用于自定义创建节点的相关操作
getPaletteEntries(element) {
const {
bpmnFactory,
create,
elementFactory,
translate
} = this;
function createTask() {}
return {
'create.pkp-task': { //名称:自定义,可随意
group: 'model', //分组,划分工具箱中节点的位置 tools 0?、event 1-3 、gateway 4、activity 5-6
className: 'pkp-task', //bpmn-icon-task: 内置图标
// className: 'bpmn-icon-user-task',
title: translate('创建一个类型为pkp-task的任务节点'),
action: { //用来决定用户点击或拖动图标时会发生什么
dragstart: createTask(),
click: createTask()
}
}
}
}
}
分组
action
完成了上面的操作, 页面就能正常渲染出一个我们自定义的元素了, 但是在点击或者拖拽的时候是没有效果的,要实现效果就是定义 action
属性
// CustomPalette.js
function createTask() {
return function(event) {
//使用elementFactory工厂提供的方法创建一个形状,
const shape = elementFactory.createShape({
type: 'bpmn:Task', //内置的圆角矩形,除此之外还有 bpmn:StartEvent、bpmn:ServiceTask、bpmn:ExclusiveGateway
});
/*
开始绘制跟随鼠标移动的节点,传入event和形状
event:原生事件event对象,提供鼠标的位置等信息,供给绘制
shape:指定绘制的形状
*/
create.start(event, shape);
}
}
使用自定义的palette
创建一个统一的index.js用于公开暴露自定义的palette
// index.js
import CustomPalette from './CustomPalette'
export default {
__init__: ['customPalette'], //__init__内的名称,在这个导出的对象中必须要有这个键,否则会报错
customPalette: ['type', CustomPalette]
}
在创建 bpmn-js 的新实例时,使用 additionalModules
属性添加自定义控件:
//使用
import BpmnModeler from 'bpmn-js/lib/Modeler';
import customControlsModule from './custom';
const bpmnModeler = new BpmnModeler({
additionalModules: [
customControlsModule
]
});
扩展-自定义palette
自定义图标
/* .css */
/* 1.自定义颜色 */
.pkp-task{
color: #cc0000 !important;
}
/* 2.自定义图标 */
.icon-custom.pkp-task { /* 加上背景图 */
border-radius: 50%;
background-size: 65%;
background-repeat: no-repeat;
background-position: center;
background-image: url('https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/rules.png');
}
完全自定义palette
将自定义的palette类,使用paletteProvider
属性去使用,即可替换掉原先默认的
// index.js
import CustomPalette from './CustomPalette'
export default {
__init__: ['paletteProvider'], //__init__内的名称,在这个导出的对象中必须要有这个键,否则会报错
paletteProvider: ['type', CustomPalette]
}
自定义renderer
仅仅只定义工具箱(palette)是不够的,绘制出来的东西还是默认的,通过自定义renderer可以自定义画布上的图形
//基本结构
import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer' // 引入默认的renderer
const HIGH_PRIORITY = 1500 // 最高优先级,默认1000
export default class CustomRenderer extends BaseRenderer { // 继承BaseRenderer
constructor(eventBus, bpmnRenderer) {
super(eventBus, HIGH_PRIORITY)
this.bpmnRenderer = bpmnRenderer
}
//用于自定义判断元素是否能被渲染
canRender(element) {
return true
}
drawShape(parentNode, element) { // 默认调用
const shape = this.bpmnRenderer.drawShape(parentNode, element)
return shape
}
getShapePath(shape) {
return this.bpmnRenderer.getShapePath(shape)
}
}
写一些配置
// util.js
const customElements = ['bpmn:Task'] // 要自定义元素的类型
const customConfig = { // 自定义元素的配置(后面会用到)
'bpmn:Task': {
'url': 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/rules.png',
'attr': { x: 0, y: 0, width: 48, height: 48 }
}
}
绘制
import {
append as svgAppend,
attr as svgAttr,
create as svgCreate
} from 'tiny-svg';
// 提供了一个默认父节点容器
function drawShape(parentNode, element) {
const type = element.type // 获取到类型
if (type==="bpmn:Task") {
// 定义参数
const attr = { x: 0, y: 0, width: 48, height: 48 }
// 创建自定义图标
const customIcon = svgCreate('image', {
...attr,
href: "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/rules.png"
})
element['width'] = attr.width
element['height'] = attr.height
// 将自定义的图标插入提供的父节点内
svgAppend(parentNode, customIcon)
// 判断是否有name属性来决定是否要渲染出label
if (element.businessObject.name) {
const text = svgCreate('text', {
x: attr.x,
y: attr.y + attr.height + 20,
"font-size": "14",
"fill": "#000"
})
text.innerHTML = element.businessObject.name
svgAppend(parentNode, text)
}
return customIcon
}
const shape = this.bpmnRenderer.drawShape(parentNode, element)
return shape
}
使用自定义的renderer
//index.js
import CustomRenderer from './CustomRenderer'
export default {
__init__: ['customRenderer'],
customRenderer: ['type', CustomRenderer]
}
//使用
...
import customModule from './custom'
...
this.bpmnModeler = new BpmnModeler({
...
additionalModules: [
// 自定义的renderer
customModule
]
})
自定义ContextPad
自定义contextPad
和palette
很像, 只不过是使用contextPad.registerProvider(this)
来指定它是一个contextPad
, 而自定义palette
是用platette.registerProvider(this)
.
// CustomContextPad.js
export default class CustomContextPad {
constructor(config, contextPad, create, elementFactory, injector, translate) {
this.create = create;
this.elementFactory = elementFactory;
this.translate = translate;
//如果没有设置自动配置为false,注入一个autoPlace的方法后面使用
if (config.autoPlace !== false) {
this.autoPlace = injector.get('autoPlace', false);
}
contextPad.registerProvider(this); // 定义这是一个contextPad
}
//操作逻辑
getContextPadEntries(element) {}
}
编写getContextPadEntries核心代码
function getContextPadEntries(element) {
function appendTask(event, element) {
if (autoPlace) {
const shape = elementFactory.createShape({ type: 'bpmn:Task' });
autoPlace.append(element, shape);
} else {
appendTaskStart(event, element);
}
}
function appendTaskStart(event) {
const shape = elementFactory.createShape({ type: 'bpmn:Task' });
create.start(event, shape, element);
}
return {
'append.pkp-task': {
group: 'model',
className: 'icon-custom pkp-task',
title: translate('创建一个类型为pkp-task的任务节点'),
action: {
click: appendTask,
dragstart: appendTaskStart
}
}
};
}
完全自定义contextPadProvider
将自定义的padProvider类,使用contextPadProvider
属性去使用,即可替换掉原先默认的
// index.js
import CustomPalette from './CustomPalette'
export default {
__init__: ['contextPadProvider'], //__init__内的名称,在这个导出的对象中必须要有这个键,否则会报错
contextPadProvider: ['type', CustomPalette]
}
编辑、删除节点篇
// CustomContextPad.js
function getContextPadEntries(element) {
const { modeling } = this // 从this获取
function removeElement(e) { // 点击的时候实现删除功能
modeling.removeElements([element])
}
function updateElementName(){
modeling.updateLabel(element,'修改后的名字')
}
return {
'delete': {
group: 'edit',
className: 'icon-custom icon-custom-delete',
title: translate('删除'),
action: {
click: removeElement
}
}
}
}
/*.css */
.icon-custom-delete {
background-image: url('https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/delete.png');
}
.icon-custom-delete.entry:hover {
background: url('https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/delete.png') center no-repeat !important;
background-size: cover !important;
}
线节点篇
// CustomContextPad.js
function getContextPadEntries(element) {
const { globalConnect } = this
return {
'global-connect-tool': { //创建一条线
group: 'tools',
className: 'icon-custom icon-custom-flow',
title: '新增线',
action: {
click: function(event){
globalConnect.toggle(event)
}
}
}
}
}
导入导出
BPMN element
:
<startEvent id="StartEvent_1y45yut"></startEvent>
const canvas = document.getElementById('canvas')
const bpmnModeler = new BpmnModeler({
container: canvas
})
// 导入xml/bpmn文件
bpmnModeler.importXML(xmlStr, (err) => {
if (!err) {
// 获取元素节点仓库
let elementRegistry = bpmnModeler.get('elementRegistry')
// 从元素节点仓库获取到元素节点
let startEventElement = elementRegistry.get('节点Id')
bpmnModeler.get('canvas').zoom('fit-viewport') //自动缩放合适比例
}
})
// 导出成svg
bpmnModeler.saveSVG(function(err, data) {
if(!err){
// 把xml转换为URI,下载要用到的
const encodedData = encodeURIComponent(data)
// 下载图的具体操作,改变a的属性,className令a标签可点击,href令能下载,download是下载的文件的名字
// 转换成file对象,可以通过URL.createObjectURL()方法将其转换为一个可以下载的
let xmlFile = new File([data], 'test.bpmn')
// 转换成a标签下载
const link = document.createElement("a");
link.className = 'active'
link.href = 'data:application/bpmn20-xml;charset=UTF-8,' + encodedData
link.download = '文件名.svg'
link.click()
}
})
// 导出成bpmn
bpmnModeler.saveXML({format:true} ,function(err, svg) {
...
})
Properties
1.什么是bpmn properties
用bpmn.js
画的每一个节点其实都被称之为diagram element
(图表元素)
而在bpmn
文件中的每个xml
标签称之为BPMN element
.
将diagram element
与BPMN element
的一些属性关联起来靠的是一个叫做businessObject
的属性. 从名称上理解你也可以知道它是一个对象(Object), 你可以在这个对象中添加上一些特殊的属性, 并且这些属性是可以直接插到BPMN element
上的.
例如一个StartEvent
节点
diagram element
:
{
id: "StartEvent_1y45yut",
type: "bpmn:StartEvent",
businessObject: {
$type: "bpmn:StartEvent",
name: "开始"
}
}
BPMN element
:
<startEvent id="StartEvent_1y45yut" name="开始"></startEvent>
2.使用js读取 bpmn properties
获取方式一:通过自定义renderer绘制图形的drawShape
方法,通过参数获取到element
对象
export default class CustomRenderer extends BaseRenderer {
drawShape (parentNode, element) {
const { businessObject } = element
}
}
获取方式二:
let bpmnJS = new BpmnJS({
container: '#canvas'
});
bpmnJS.importXML(xmlStr, function(err) {
if (!err) {
let elementRegistry = bpmnJs.get('elementRegistry')
let startEventElement = elementRegistry.get('StartEvent_1y45yut'),
startEvent = startEventElement.businessObject;
console.log(startEvent.name) // 开始
}
}
3.使用js修改 bpmn properties
<bpmn:sequenceFlow id="SequenceFlow_1" sourceRef="StartEvent_1" targetRef="Task_1" name="FOO > BAR?">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">
<![CDATA[${foo > bar}]]>
</bpmn:conditionExpression>
</bpmn:sequenceFlow>
手动创建元素
let moddle = bpmnJS.get('moddle');
// 创建一个BPMN element , 并且载入到导出的xml里
let newCondition = moddle.create('bpmn:FormalExpression', {
body: '${ value > 100 }'
});
写入元素
// 写入属性, 但是不支持撤销
sequenceFlow.conditionExpression = newCondition;
修改已有元素
let modeling = bpmnJS.get('modeling');
// 参数:目标元素 修改的内容
modeling.updateProperties(sequenceFlowElement, {
conditionExpression: newCondition
});
阅读ruoyi-flowable-plus
一、画布操作
1.对齐
const Align = bpmnModeler.get("alignElements") //提供对齐操作的相关方法
const Selection = bpmnModeler.get("selection"); //用于获取当前选中的元素信息
const SelectedElements = Selection.get(); //获取所有选中的节点
Align.trigger(SelectedElements, 'left')); //对齐方式:left right top bottom center middle
2.缩放
bpmnModeler.get("canvas").zoom(0.2);
bpmnModeler.get("canvas").zoom('fit-viewport') //自动缩放合适比例
bpmnModeler.get("canvas").zoom('fit-viewport','auto') //自动缩放合适比例并居中
3.撤销
操作栈:commandStack
bpmnModeler.get("commandStack")
//获取操作栈对象
- canRedo 是否可恢复
- canUndo 是否可撤销
- redo 恢复上一步操作
- undo 撤销上一步操作
4.清空
bpmnModeler.clear()
二、数据修改
要使生成的bpmn结构是正确的,进行数据修改本质上就是操作businessObject对象,而是要通过bpmn提供的方法去创建ModdleElement节点
例如:要新增一个 ikuaijin:element 属性节点
const listenerObj = Object.create(null);
listenerObj.name="你好"
// 创建一个节点
const moddle = bpmnModeler.get("moddle")
const moddleElement = moddle.create(`hello:element`,listenerObj)
// 获取跟节点
const rootElements = bpmnModeler.getDefinitions().rootElements
rootElements.push(moddleElement)
// 修改节点属性
const moddling = bpmnModeler.get("modeling")
moddling.updateProperties
总结
操作对象汇总
获取操作方法的步骤:bpmnModeler.get("名称")
名称 | 说明 | 方法 |
---|---|---|
alignElements | 提供对齐操作的相关方法 | trigger(选中的元素arr, 对齐方式)) 对齐方式:left right top bottom center middle |
selection | 获取当前选中的元素对象信息 | get() 当前选中的元素信息 |
canvas | 可对画布相关操作 | zoom() 缩放画布 |
commandStack | 操作栈 | canRedo() 是否可恢复 canUndo() 是否可撤销 redo() 恢复上一步操作 undo() 撤销上一步操作 |
modeling | 对节点相关操作 | removeElements(元素) 移除元素 updateProperties(元素,内容) 修改元素属性 |
moddle | 提供节点创建方法 | create(标签名,内容) |