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

前言

Q: bpmn.js是什么? 🤔️

bpmn.js是一个BPMN2.0渲染工具包和web建模器, 使得画流程图的功能在前端来完成.

Q: 我为什么要写该系列的教材? 🤔️

因为公司业务的需要因而要在项目中使用到bpmn.js,但是由于bpmn.js的开发者是国外友人, 因此国内对这方面的教材很少, 也没有详细的文档. 所以很多使用方式很多坑都得自己去找.在将其琢磨完之后, 决定写一系列关于它的教材来帮助更多bpmn.js的使用者或者是期于找到一种好的绘制流程图的开发者. 同时也是自己对其的一种巩固.

由于是系列的文章, 所以更新的可能会比较频繁, 您要是无意间刷到了且不是您所需要的还请谅解😊.

不求赞👍不求心❤️. 只希望能对你有一点小小的帮助.

自定义Renderer篇

接着上一章节, 我们已经知道了该如何自定义左侧的工具栏(Palette), 不了解的小伙伴可以移步: 《全网最详bpmn.js教材-自定义palette篇》.

但是同时我们也知道仅仅只改变Palette是不够的, 因为绘画出来的图形还是“裸体的”:

image

这一章节我们就来看一下如何自定义画布上的图形, 也就是实现自定义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的节点就被渲染成了自定义的“黄金积木”😝:

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的属性, 如下图:

image

开始节点和lindaidai-task中都有name属性, 但是在bpmn:StartEvent上能将这个label显示出来, 是因为在下面有一个bpmndi:BPMNLabel的标签.

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

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

看起来好像成功了 ! good boy ! 😄

但是当我双击想要去编辑label文字的时候, 却出现了这样的效果:

image

它直接在我原来图形的上面新建了一个输入框...

额😅...其实我也没有想到什么好的办法去解决,在这里我提供一个看起来可行的方案:
在双击元素的时候, 将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教材-编辑、删除节点篇》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容