全网最详bpmn.js教材-自定义renderer篇

27,295 阅读5分钟

前言

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.js文件, 用来自定义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的节点就被渲染成了自定义的“黄金积木”😝: bpmnCustom9.png

完全自定义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的标签.

于是就造成了图形上是这样显示的:

bpmn11.png
bpmn11.png

那么我们该如何将这里的label显示出来呢?

首先让我们先将Shape打印出来看看: bpmn12.png

可以发现在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来判断好了😓...

打开页面效果是这样的:

bpmn13.png
bpmn13.png

看起来好像成功了 ! 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教材》

系列相关推荐:

《全网最详bpmn.js教材-基础篇》

《全网最详bpmn.js教材-http请求篇》

《全网最详bpmn.js教材-事件篇》

《全网最详bpmn.js教材-contextPad篇》

《全网最详bpmn.js教材-编辑、删除节点篇》