Q: bpmn.js是什么? 🤔️javascript
”
bpmn.js是一个BPMN2.0渲染工具包和web建模器, 使得画流程图的功能在前端来完成.html
Q: 我为何要写该系列的教材? 🤔️前端
”
由于公司业务的须要于是要在项目中使用到bpmn.js
,可是因为bpmn.js
的开发者是国外友人, 所以国内对这方面的教材不多, 也没有详细的文档. 因此不少使用方式不少坑都得本身去找.在将其琢磨完以后, 决定写一系列关于它的教材来帮助更多bpmn.js
的使用者或者是期于找到一种好的绘制流程图的开发者. 同时也是本身对其的一种巩固.vue
因为是系列的文章, 因此更新的可能会比较频繁, 您要是无心间刷到了且不是您所须要的还请谅解😊.java
不求赞👍不求心❤️. 只但愿能对你有一点小小的帮助.ios
在进入这一章节的学习以前, 我但愿你能先掌握前面几节的知识点: 自定义palette
、自定义renderer
、自定义contextPad
、编辑删除节点.git
由于这一章节会将前面几节的内容作一个汇总, 而后提供一个可用的bpmn
组件解决方案.github
经过阅读你能够学习到:web
首先让咱们先来了解一下线节点是如何建立的.canvas
我以CustomPalette.js
为例子🌰, 还记得在以前讲的createTask
吗, 建立线和它差很少:
// CustomPalette.js
PaletteProvider.$inject = [
...
'globalConnect'
]
PaletteProvider.prototype.getPaletteEntries = function(element) {
const { globalConnect } = this
function createConnect () {
return {
group: 'tools',
className: 'icon-custom icon-custom-flow',
title: '新增线',
action: {
click: function (event) {
globalConnect.toggle(event)
}
}
}
}
return {
'create.lindaidai-task': {...},
'global-connect-tool': createConnect()
}
}
复制代码
这样就能够画出线了:
通过了上面那么的例子, 其实咱们不难发现, 在每一个关键的函数中, 都是将本身想要自定义的东西经过函数返回值传递出去.
并且返回值的内容都大同小异, 无非就是group、className
等等东西, 那么这样的话, 咱们是否是能够将其整合一下, 减小许多代码量呢?
咱们能够构建这样一个函数:
// CustomPalette.js
function createAction (type, group, className, title, options) {
function createListener (event) {
var shape = elementFactory.createShape(assign({ type }, options))
create.start(event, shape)
}
return {
group,
className,
title: '新增' + title,
action: {
dragstart: createListener,
click: createListener
}
}
}
复制代码
它接收全部元素不一样的属性, 而后返回一个自定义元素.
可是线的建立可能有些不一样:
// CustomPalette.js
function createConnect (type, group, className, title, options) {
return {
group,
className,
title: '新增' + title,
action: {
click: function (event) {
globalConnect.toggle(event)
}
}
}
}
复制代码
所以我这里把建立元素的函数分为两类: createAction
和createConnect
.
接下来咱们只须要构建一个这样的数组:
// utils/util.js
const flowAction = { // 线
type: 'global-connect-tool',
action: ['bpmn:SequenceFlow', 'tools', 'icon-custom icon-custom-flow', '链接线']
}
const customShapeAction = [ // shape
{
type: 'create.start-event',
action: ['bpmn:StartEvent', 'event', 'icon-custom icon-custom-start', '开始节点']
},
{
type: 'create.end-event',
action: ['bpmn:EndEvent', 'event', 'icon-custom icon-custom-end', '结束节点']
},
{
type: 'create.task',
action: ['bpmn:Task', 'activity', 'icon-custom icon-custom-task', '普通任务']
},
{
type: 'create.businessRule-task',
action: ['bpmn:BusinessRuleTask', 'activity', 'icon-custom icon-custom-businessRule', 'businessRule任务']
},
{
type: 'create.exclusive-gateway',
action: ['bpmn:ExclusiveGateway', 'activity', 'icon-custom icon-custom-exclusive-gateway', '网关']
},
{
type: 'create.dataObjectReference',
action: ['bpmn:DataObjectReference', 'activity', 'icon-custom icon-custom-data', '变量']
}
]
const customFlowAction = [
flowAction
]
export { customShapeAction, customFlowAction }
复制代码
同时构建一个方法来循环建立出上面👆的元素:
// utils/util.js
/** * 循环建立出一系列的元素 * @param {Array} actions 元素集合 * @param {Object} fn 处理的函数 */
export function batchCreateCustom(actions, fn) {
const customs = {}
actions.forEach(item => {
customs[item['type']] = fn(...item['action'])
})
return customs
}
复制代码
CustomPalette.js
代码以后就能够在CustomPalette.js
中来引用它们了:
// CustomPalette.js
import { customShapeAction, customFlowAction, batchCreateCustom } from './../../utils/util'
PaletteProvider.prototype.getPaletteEntries = function(element) {
var actions = {}
const {
create,
elementFactory,
globalConnect
} = this;
function createConnect(type, group, className, title, options) {
return {
group,
className,
title: '新增' + title,
action: {
click: function(event) {
globalConnect.toggle(event)
}
}
}
}
function createAction(type, group, className, title, options) {
function createListener(event) {
var shape = elementFactory.createShape(Object.assign({ type }, options))
create.start(event, shape)
}
return {
group,
className,
title: '新增' + title,
action: {
dragstart: createListener,
click: createListener
}
}
}
Object.assign(actions, {
...batchCreateCustom(customFlowAction, createConnect), // 线
...batchCreateCustom(customShapeAction, createAction)
})
return actions
}
复制代码
这样看来代码是否是精简不少了呢😊.
让咱们来看看页面的效果:
此时左侧的工具栏就已经所有被替换成咱们想要的图片了.
CustomRenderer.js
代码而后就到了编写renderer
代码的时候了, 在编写以前, 一样的, 咱们能够作一些配置项.
由于咱们注意到在渲染自定义元素的的时候, 靠的就是svgCreate('image', {})
这个方法.
它里面也是接收的一个图片的地址url
和样式配置attr
.
那么url
的前缀咱们就能够提取出来:
// utils/util.js
const STATICPATH = 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/' // 静态文件路径
const customConfig = { // 自定义元素的配置
'bpmn:StartEvent': {
'field': 'start',
'title': '开始节点',
'attr': { x: 0, y: 0, width: 40, height: 40 }
},
'bpmn:EndEvent': {
'field': 'end',
'title': '结束节点',
'attr': { x: 0, y: 0, width: 40, height: 40 }
},
'bpmn:SequenceFlow': {
'field': 'flow',
'title': '链接线',
},
'bpmn:Task': {
'field': 'rules',
'title': '普通任务',
'attr': { x: 0, y: 0, width: 48, height: 48 }
},
'bpmn:BusinessRuleTask': {
'field': 'variable',
'title': 'businessRule任务',
'attr': { x: 0, y: 0, width: 48, height: 48 }
},
'bpmn:ExclusiveGateway': {
'field': 'decision',
'title': '网关',
'attr': { x: 0, y: 0, width: 48, height: 48 }
},
'bpmn:DataObjectReference': {
'field': 'score',
'title': '变量',
'attr': { x: 0, y: 0, width: 48, height: 48 }
}
}
const hasLabelElements = ['bpmn:StartEvent', 'bpmn:EndEvent', 'bpmn:ExclusiveGateway', 'bpmn:DataObjectReference'] // 一开始就有label标签的元素类型
export { STATICPATH, customConfig, hasLabelElements }
复制代码
而后只须要在编写drawShape
方法的时候判断一下就能够了:
// CustomRenderer.js
import inherits from 'inherits'
import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'
import {
append as svgAppend,
create as svgCreate
} from 'tiny-svg'
import { customElements, customConfig, STATICPATH, hasLabelElements } from '../../utils/util'
/** * A renderer that knows how to render custom elements. */
export default function CustomRenderer(eventBus, styles, bpmnRenderer) {
BaseRenderer.call(this, eventBus, 2000)
var computeStyle = styles.computeStyle
this.drawElements = function(parentNode, element) {
console.log(element)
const type = element.type // 获取到类型
if (type !== 'label') {
if (customElements.includes(type)) { // or customConfig[type]
return drawCustomElements(parentNode, element)
}
const shape = bpmnRenderer.drawShape(parentNode, element)
return shape
} else {
element
}
}
}
inherits(CustomRenderer, BaseRenderer)
CustomRenderer.$inject = ['eventBus', 'styles', 'bpmnRenderer']
CustomRenderer.prototype.canRender = function(element) {
// ignore labels
return true
// return !element.labelTarget;
}
CustomRenderer.prototype.drawShape = function(parentNode, element) {
return this.drawElements(parentNode, element)
}
CustomRenderer.prototype.getShapePath = function(shape) {
// console.log(shape)
}
function drawCustomElements(parentNode, element) {
const { type } = element
const { field, attr } = customConfig[type]
const url = `${STATICPATH}${field}.png`
const customIcon = svgCreate('image', {
...attr,
href: url
})
element['width'] = attr.width // 这里我是取了巧, 直接修改了元素的宽高
element['height'] = attr.height
svgAppend(parentNode, customIcon)
// 判断是否有name属性来决定是否要渲染出label
if (!hasLabelElements.includes(type) && 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
}
复制代码
关键在于drawCustomElements
函数中, 利用了url
的一个字符串拼接.
这样的话, 自定义元素就能够都渲染出来了.
效果以下:
CustomContextProvider.js
代码完成了palette
和renderer
的编写, 接下来让咱们看看contextPad
是怎么编写的.
其实它的写法和palette
差很少, 只不过有一点须要咱们注意的:
不一样类型的节点出现的contextPad
的内容多是不一样的.
好比:
StartEvent
会出现
edit、delete、Task、BusinessRuleTask、ExclusiveGateway
等等;
EndEvent
只能出现
edit、delete
;
SequenceFlow
只能出现
edit、delete
.
也就是说咱们须要根据节点类型来返回不一样的contextPad
.
那么在编写getContextPadEntries
函数返回值的时候, 就能够根据element.type
来返回不一样的结果:
import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil'
ContextPadProvider.prototype.getContextPadEntries = function(element) {
... // 此处省略的代码可查看项目github源码
// 只有点击列表中的元素才会产生的元素
if (isAny(businessObject, ['bpmn:StartEvent', 'bpmn:Task', 'bpmn:BusinessRuleTask', 'bpmn:ExclusiveGateway', 'bpmn:DataObjectReference'])) {
Object.assign(actions, {
...batchCreateCustom(customShapeAction, createAction),
...batchCreateCustom(customFlowAction, createConnect), // 链接线
'edit': editElement(),
'delete': deleteElement()
})
}
// 结束节点和线只有删除和编辑
if (isAny(businessObject, ['bpmn:EndEvent', 'bpmn:SequenceFlow', 'bpmn:DataOutputAssociation'])) {
Object.assign(actions, {
'edit': editElement(),
'delete': deleteElement()
})
}
return actions
}
复制代码
isAny
的做用其实就是判断类型属不属于后面数组中, 相似于includes
.
这样咱们的contextPad
就丰富起来了😊.
有了自定义modeler
的基础, 咱们就能够将bpmn
封装成一个组件, 在咱们须要应用的地方引用这个组件就能够了.
为了给你们更好演示, 我新建了一个项目 bpmn-custom-modeler , 里面的依赖和配置都和 bpmn-vue-custom中相同, 只不过在这个新的项目里我是打算用自定义的modeler
来覆盖它原有的, 并封装一个bpmn
组件来供页面使用.
在项目的components
文件夹下新建一个名为bpmn
的文件夹, 这里面用来存放封装的bpmn
组件.
而后咱们还能够准备一个空的xml
做为组件中的默认显示(也就是如果一进来没有任何图形的时候应该显示的是什么内容), 这里我定义了一个newDiagram.js
.
再在根目录下建立一个views
文件来放一些页面文件, 这里我就再新建一个custom-modeler.vue
用来引用封装好的bpmn
组件来看效果.
props
首先让咱们来思考一下, 既然要把它封装成组件, 那么确定是须要给这个组件里传递props
(能够理解为参数). 它能够是一整个xml
字符串, 也能够是一个bpmn
文件的地址.
我以传入bpmn
文件地址为例进行封装. 固然大家能够根据本身的业务需求来定.
也就是在引用这个组件的时候, 我指望的是这样写:
/* views/custom-modeler.vue */
<template>
<bpmn :xmlUrl="xmlUrl" @change="changeBpmn"></bpmn>
</template>
<script> import { Bpmn } from './../components/bpmn' export default { components: { Bpmn }, data () { return { xmlUrl: 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmnMock.bpmn' } }, methods: { changeBpmn ($event) {} } } </script>
复制代码
只要引用了bpmn
组件, 而后传递一个url
, 页面上就能够显示出对应的图形内容.
这样的话, 咱们的Bpmn.vue
中就应该这样定义props
:
// Bpmn.vue
props: {
xmlUrl: {
type: String,
default: ''
}
}
复制代码
hmtl
代码组件中的html
代码十分容易, 主要是给画布一个盛放的容器, 再定义了两个按钮用于下载:
<!-- Bpmn.vue -->
<template>
<div class="containers">
<div class="canvas" ref="canvas"></div>
<div id="js-properties-panel" class="panel"></div>
<ul class="buttons">
<li>
<a ref="saveDiagram" href="javascript:" title="保存为bpmn">保存为bpmn</a>
</li>
<li>
<a ref="saveSvg" href="javascript:" title="保存为svg">保存为svg</a>
</li>
</ul>
</div>
</template>
复制代码
js
代码在js
里, 我就将前面几节《全网最详bpmn.js教材-http请求篇》 和 《全网最详bpmn.js教材-http事件篇》 中的功能都整合了进来.
大致就是:
xmlUrl
作判断, 如果不为空的话则请求获取数据,不然赋值一个默认值;
modeler
、
element
的监听事件;
xml、svg
的连接按钮.
例如:
// Bpmn.vue
async createNewDiagram () {
const that = this
let bpmnXmlStr = ''
if (this.xmlUrl === '') { // 判断是否存在
bpmnXmlStr = this.defaultXmlStr
this.transformCanvas(bpmnXmlStr)
} else {
let res = await axios({
method: 'get',
timeout: 120000,
url: that.xmlUrl,
headers: { 'Content-Type': 'multipart/form-data' }
})
console.log(res)
bpmnXmlStr = res['data']
this.transformCanvas(bpmnXmlStr)
}
},
transformCanvas(bpmnXmlStr) {
// 将字符串转换成图显示出来
this.bpmnModeler.importXML(bpmnXmlStr, (err) => {
if (err) {
console.error(err)
} else {
this.success()
}
// 让图能自适应屏幕
var canvas = this.bpmnModeler.get('canvas')
canvas.zoom('fit-viewport')
})
},
success () {
this.addBpmnListener()
this.addModelerListener()
this.addEventBusListener()
},
addBpmnListener () {},
addModelerListener () {},
addEventBusListener () {}
复制代码
整合以后的代码有些多, 这里贴出来有点不太好, 详细代码在gitHub
上有: LinDaiDai/bpmn-custom-modeler/Bpmn.vue
项目案例Git地址: LinDaiDai/bpmn-custom-modeler 喜欢的小伙伴请给个Star
🌟呀, 谢谢😊
系列所有目录请查看此处: 《全网最详bpmn.js教材》
系列相关推荐: