前言
Q: bpmn.js是什么? 🤔️
bpmn.js是一个BPMN2.0渲染工具包和web建模器, 使得画流程图的功能在前端来完成.
Q: 我为什么要写该系列的教材? 🤔️
因为公司业务的需要因而要在项目中使用到bpmn.js
,但是由于bpmn.js
的开发者是国外友人, 因此国内对这方面的教材很少, 也没有详细的文档. 所以很多使用方式很多坑都得自己去找.在将其琢磨完之后, 决定写一系列关于它的教材来帮助更多bpmn.js
的使用者或者是期于找到一种好的绘制流程图的开发者. 同时也是自己对其的一种巩固.
由于是系列的文章, 所以更新的可能会比较频繁, 您要是无意间刷到了且不是您所需要的还请谅解😊.
不求赞👍不求心❤️. 只希望能对你有一点小小的帮助.
自定义Renderer篇
接着上一章节, 我们已经知道了该如何自定义左侧的工具栏(Palette), 不了解的小伙伴可以移步: 《全网最详bpmn.js教材-自定义palette篇》.
但是同时我们也知道仅仅只改变Palette
是不够的, 因为绘画出来的图形还是“裸体的”:
这一章节我们就来看一下如何自定义画布上的图形, 也就是实现自定义Renderer
的功能.
通过阅读你可以学习到:
在默认的Renderer基础上修改
和自定义Palette
一样, 先来看看最简单的在原有的元素上进行修改.
前期准备
让我们接着在LinDaiDai/bpmn-vue-custom案例项目上进行开发.
在components
文件夹下新建一个custom-renderer.vue
文件, 同时配置路由“自定义renderer”.
在components/custom
文件夹下新建一个CustomRenderer.vue
文件, 用来自定义renderer
.
在components
文件夹下新建一个utils
文件夹同时新建util.js
文件, 用来放一些公共的方法和配置.
编写CustomRenderer.vue
代码
由于是在bpmn.js
已有的元素上进行修改, 所以首先我们可以先将BaseRenderer
这个类引入进来, 然后让我们的自定义renderer
继承它:
import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer' // 引入默认的renderer
const HIGH_PRIORITY = 1500 // 最高优先级
export default class CustomRenderer extends BaseRenderer { // 继承BaseRenderer
constructor(eventBus, bpmnRenderer) {
super(eventBus, HIGH_PRIORITY)
this.bpmnRenderer = bpmnRenderer
}
canRender(element) {
// ignore labels
return !element.labelTarget
}
drawShape(parentNode, element) { // 核心函数就是绘制shape
const shape = this.bpmnRenderer.drawShape(parentNode, element)
return shape
}
getShapePath(shape) {
return this.bpmnRenderer.getShapePath(shape)
}
}
CustomRenderer.$inject = ['eventBus', 'bpmnRenderer']
上面👆的代码很简单, 相信大家都可以看的明白.
注: 这里有个小坑要注意一下, 就是HIGH_PRIORITY
不能够去掉, 不然的话你会发现它不会执行下面的drawShpe
函数
到了这里可能就有小伙伴要问了, 感觉你做了这么多并没有什么用啊, 还是没有看到关于自定义renderer
的效果呀😅!
没错, 只完成上面的步骤那是不够的, 关键是在于如何编写drawShape
这个方法.
编写drawShape
代码
我们可以先在前面创建好的utils/util.js
文件下写下此代码:
// util.js
const customElements = ['bpmn:Task']
export { customElements }
也就是创建了一个名为customElements
的数组然后导出, 至于数组里为什么只有一项bpmn:Task
?🤔️
那是因为在上一个案例中我创建的lindaidai-task
的类型就是bpmn:Task
类型的.
所以这个数组的作用就是用来放哪些类型是需要我们自定义的, 从而在渲染的时候就可以与不需要自定义的元素作区分.
甚至你还可以做一些配置:
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 }
}
}
export { customElements, customConfig }
让我们在CustomRenderer.js
中使用并编写它:
import { customElements, customConfig } from '../utils/util'
...
drawShape(parentNode, element) {
const type = element.type // 获取到类型
if (customElements.includes(type)) { // or customConfig[type]
const { url, attr } = customConfig[type]
const customIcon = svgCreate('image', { // 在这里创建了一个image
...attr,
href: url
})
element['width'] = attr.width // 这里我是取了巧, 直接修改了元素的宽高
element['height'] = attr.height
svgAppend(parentNode, customIcon)
return customIcon
}
const shape = this.bpmnRenderer.drawShape(parentNode, element)
return shape
}
...
可以看到,实现让页面渲染出自己想要的效果的做法就是使用svgCreate
方法创建一个image
并添加到父节点中.
导出并使用CustomRenderer
同样的自定义renderer
需要导出才能使用, 修改custom/index.js
文件:
import CustomPalette from './CustomPalette'
import CustomRenderer from './CustomRenderer'
export default {
__init__: ['customPalette', 'customRenderer'],
customPalette: ['type', CustomPalette],
customRenderer: ['type', CustomRenderer]
}
注意: __init__
中的属性命名customRenderer
都是固定的写法不能修改, 不然就会没有效果
要是你看了之前custom-palette.vue
的话, 就知道直接在页面上应用就行了:
<!--custom-renderer.vue-->
<script>
...
import customModule from './custom'
...
this.bpmnModeler = new BpmnModeler({
...
additionalModules: [
// 左边工具栏以及节点
propertiesProviderModule,
// 自定义的节点
customModule
]
})
注意: 项目案例里我为了方便演示, 在custom-palette
中引入的是ImportJS/onlyRenderer.js
, 而上面的案例是以引入custom/index.js
为讲解的, 这个自己要明白如何区分.
此时打开页面就可以看到效果了, 类型为bpmn:Task
的节点就被渲染成了自定义的“黄金积木”😝:
完全自定义Renderer
完全自定义Renderer
的意思就是将原本使用new BpmnModeler
创建画布的方式改为使用new CustomModeler
来创建.
这一部分在《全网最详bpmn.js教材-自定义palette篇》中讲解的很详细了, 就不做过多的阐述.
同样是在customModeler/custom
的文件夹下创建一个customRender.js
文件, 然后写入以下代码:
/* eslint-disable no-unused-vars */
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 } from '../../utils/util'
/**
* A renderer that knows how to render custom elements.
*/
export default function CustomRenderer(eventBus, styles) {
BaseRenderer.call(this, eventBus, 2000)
var computeStyle = styles.computeStyle
this.drawCustomElements = function(parentNode, element) {
console.log(element)
const type = element.type // 获取到类型
if (customElements.includes(type)) { // or customConfig[type]
const { url, attr } = customConfig[type]
const customIcon = svgCreate('image', {
...attr,
href: url
})
element['width'] = attr.width // 这里我是取了巧, 直接修改了元素的宽高
element['height'] = attr.height
svgAppend(parentNode, customIcon)
return customIcon
}
const shape = this.bpmnRenderer.drawShape(parentNode, element)
return shape
}
}
inherits(CustomRenderer, BaseRenderer)
CustomRenderer.$inject = ['eventBus', 'styles']
CustomRenderer.prototype.canRender = function(element) {
// ignore labels
return !element.labelTarget;
}
CustomRenderer.prototype.drawShape = function(p, element) {
return this.drawCustomElements(p, element)
}
CustomRenderer.prototype.getShapePath = function(shape) {
console.log(shape)
}
直接修改原型链中的drawShape
方法就可以了.
然后记得在customModeler/custom/index.js
中将其导出.
label标签自定义在元素下方
由于评论区有小伙伴提了问题: 该如何将label
标签自定义在元素的下方?
因此霖呆呆我回去也是花了点时间研究了一下label
标签.
首先label
标签实际上是xml
中各个标签上的一个名叫name
的属性, 如下图:
开始节点和lindaidai-task
中都有name
属性, 但是在bpmn:StartEvent
上能将这个label
显示出来, 是因为在下面有一个bpmndi:BPMNLabel
的标签.
于是就造成了图形上是这样显示的:
那么我们该如何将这里的label
显示出来呢?
首先让我们先将Shape
打印出来看看:
可以发现在businessObject
中有一个name
属性...
既然这样的话, 我们肯定也能在drawShape
中拿到这个name
属性, 之后可以用svgCreate
方法给父节点中添加一个文本类型的标签.
// CustomRenderer.js
import { hasLabelElements } from '../../utils/util'
drawShape(parentNode, element) {
const type = element.type // 获取到类型
if (customElements.includes(type)) { // or customConfig[type]
const { url, attr } = customConfig[type]
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, // y取的是父元素的y+height+20
"font-size": "14",
"fill": "#000"
})
text.innerHTML = element.businessObject.name
svgAppend(parentNode, text)
console.log(text)
}
return customIcon
}
const shape = this.bpmnRenderer.drawShape(parentNode, element)
return shape
}
因为有些元素本身就带有label
属性的, 比如bpmn:StartEvent
, 所以不需要重新渲染, 因此我在util.js
中加了一个hasLabelElements
数组:
// utils/util.js
const hasLabelElements = ['bpmn:StartEvent', 'bpmn:EndEvent'] // 一开始就有label标签的元素类型
之前我是想通过element.labels.length<=0
来过滤掉开始就有label
标签的元素的, 但是发现在渲染阶段还获取不到labels
, 所以长度一直都会是0
, 就干脆定义一个hasLabelElements
来判断好了😓...
打开页面效果是这样的:
看起来好像成功了 ! good boy ! 😄
但是当我双击想要去编辑label
文字的时候, 却出现了这样的效果:
它直接在我原来图形的上面新建了一个输入框...
额😅...其实我也没有想到什么好的办法去解决,在这里我提供一个看起来可行的方案:
在双击元素的时候, 将text
给移除, 或者将他的innerHTML
设置为''
.
当然你要是感觉这样也看得下去的话, 咱不捣鼓也行, 毕竟你编辑这里面的内容, 下面的label
也是会同步的变的.
再不济的话, 你可以全局修改djs-direct-editing-parent
这个类的样式, 将下面的文字给覆盖上也是可以的... 当然感觉这个不是一个很好的办法.
在app.css
中写入:
.djs-direct-editing-parent {
top: 130px!important;
width: 60px!important;
}
总结
上面的做法主要是利用svgCreate
来创建text
元素添加到parentNode
中, 其实bpmn.js
中用到了很多ting-svg
的东西, 之前也没接触过这些, 然后也是通过查找资料了解到svgCreate
的用法...
科普一波好了, 哈哈😄:
SVG基础知识
后语
上面👆案例用的都是同一个项目🦐
项目案例Git地址: LinDaiDai/bpmn-vue-custom 喜欢的小伙伴请给个Star
🌟呀, 谢谢😊
系列全部目录请查看此处: 《全网最详bpmn.js教材》
系列相关推荐: