全網(wǎng)最詳bpmn.js教材-自定義renderer篇

前言

Q: bpmn.js是什么? ???

bpmn.js是一個(gè)BPMN2.0渲染工具包和web建模器, 使得畫流程圖的功能在前端來(lái)完成.

Q: 我為什么要寫該系列的教材? ???

因?yàn)楣緲I(yè)務(wù)的需要因而要在項(xiàng)目中使用到bpmn.js,但是由于bpmn.js的開發(fā)者是國(guó)外友人, 因此國(guó)內(nèi)對(duì)這方面的教材很少, 也沒有詳細(xì)的文檔. 所以很多使用方式很多坑都得自己去找.在將其琢磨完之后, 決定寫一系列關(guān)于它的教材來(lái)幫助更多bpmn.js的使用者或者是期于找到一種好的繪制流程圖的開發(fā)者. 同時(shí)也是自己對(duì)其的一種鞏固.

由于是系列的文章, 所以更新的可能會(huì)比較頻繁, 您要是無(wú)意間刷到了且不是您所需要的還請(qǐng)諒解??.

不求贊??不求心??. 只希望能對(duì)你有一點(diǎn)小小的幫助.

自定義Renderer篇

接著上一章節(jié), 我們已經(jīng)知道了該如何自定義左側(cè)的工具欄(Palette), 不了解的小伙伴可以移步: 《全網(wǎng)最詳bpmn.js教材-自定義palette篇》.

但是同時(shí)我們也知道僅僅只改變Palette是不夠的, 因?yàn)槔L畫出來(lái)的圖形還是“裸體的”:

image

這一章節(jié)我們就來(lái)看一下如何自定義畫布上的圖形, 也就是實(shí)現(xiàn)自定義Renderer的功能.

通過閱讀你可以學(xué)習(xí)到:

在默認(rèn)的Renderer基礎(chǔ)上修改

和自定義Palette一樣, 先來(lái)看看最簡(jiǎn)單的在原有的元素上進(jìn)行修改.

前期準(zhǔn)備

讓我們接著在LinDaiDai/bpmn-vue-custom案例項(xiàng)目上進(jìn)行開發(fā).

components文件夾下新建一個(gè)custom-renderer.vue文件, 同時(shí)配置路由“自定義renderer”.

components/custom文件夾下新建一個(gè)CustomRenderer.vue文件, 用來(lái)自定義renderer.

components文件夾下新建一個(gè)utils文件夾同時(shí)新建util.js文件, 用來(lái)放一些公共的方法和配置.

編寫CustomRenderer.vue代碼

由于是在bpmn.js已有的元素上進(jìn)行修改, 所以首先我們可以先將BaseRenderer這個(gè)類引入進(jìn)來(lái), 然后讓我們的自定義renderer繼承它:

import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer' // 引入默認(rèn)的renderer
const HIGH_PRIORITY = 1500 // 最高優(yōu)先級(jí)
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) { // 核心函數(shù)就是繪制shape
        const shape = this.bpmnRenderer.drawShape(parentNode, element)
        return shape
    }

    getShapePath(shape) {
        return this.bpmnRenderer.getShapePath(shape)
    }
}

CustomRenderer.$inject = ['eventBus', 'bpmnRenderer']

上面??的代碼很簡(jiǎn)單, 相信大家都可以看的明白.

注: 這里有個(gè)小坑要注意一下, 就是HIGH_PRIORITY不能夠去掉, 不然的話你會(huì)發(fā)現(xiàn)它不會(huì)執(zhí)行下面的drawShpe函數(shù)

到了這里可能就有小伙伴要問了, 感覺你做了這么多并沒有什么用啊, 還是沒有看到關(guān)于自定義renderer的效果呀??!

沒錯(cuò), 只完成上面的步驟那是不夠的, 關(guān)鍵是在于如何編寫drawShape這個(gè)方法.

編寫drawShape代碼

我們可以先在前面創(chuàng)建好的utils/util.js文件下寫下此代碼:

// util.js
const customElements = ['bpmn:Task']

export { customElements }

也就是創(chuàng)建了一個(gè)名為customElements的數(shù)組然后導(dǎo)出, 至于數(shù)組里為什么只有一項(xiàng)bpmn:Task????

那是因?yàn)樵谏弦粋€(gè)案例中我創(chuàng)建的lindaidai-task的類型就是bpmn:Task類型的.

所以這個(gè)數(shù)組的作用就是用來(lái)放哪些類型是需要我們自定義的, 從而在渲染的時(shí)候就可以與不需要自定義的元素作區(qū)分.

甚至你還可以做一些配置:

const customElements = ['bpmn:Task'] // 自定義元素的類型
const customConfig = { // 自定義元素的配置(后面會(huì)用到)
    '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 }

讓我們?cè)?code>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', { // 在這里創(chuàng)建了一個(gè)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
    }
...

可以看到,實(shí)現(xiàn)讓頁(yè)面渲染出自己想要的效果的做法就是使用svgCreate方法創(chuàng)建一個(gè)image并添加到父節(jié)點(diǎn)中.

導(dǎo)出并使用CustomRenderer

同樣的自定義renderer需要導(dǎo)出才能使用, 修改custom/index.js文件:

import CustomPalette from './CustomPalette'
import CustomRenderer from './CustomRenderer'

export default {
    __init__: ['customPalette', 'customRenderer'],
    customPalette: ['type', CustomPalette],
    customRenderer: ['type', CustomRenderer]
}

注意: __init__中的屬性命名customRenderer都是固定的寫法不能修改, 不然就會(huì)沒有效果

要是你看了之前custom-palette.vue的話, 就知道直接在頁(yè)面上應(yīng)用就行了:

<!--custom-renderer.vue-->
<script>
...
import customModule from './custom'
...
this.bpmnModeler = new BpmnModeler({
...
    additionalModules: [
        // 左邊工具欄以及節(jié)點(diǎn)
        propertiesProviderModule,
        // 自定義的節(jié)點(diǎn)
        customModule
    ]
})

注意: 項(xiàng)目案例里我為了方便演示, 在custom-palette中引入的是ImportJS/onlyRenderer.js, 而上面的案例是以引入custom/index.js為講解的, 這個(gè)自己要明白如何區(qū)分.

此時(shí)打開頁(yè)面就可以看到效果了, 類型為bpmn:Task的節(jié)點(diǎn)就被渲染成了自定義的“黃金積木”??:

bpmnCustom9.png

完全自定義Renderer

完全自定義Renderer的意思就是將原本使用new BpmnModeler創(chuàng)建畫布的方式改為使用new CustomModeler來(lái)創(chuàng)建.

這一部分在《全網(wǎng)最詳bpmn.js教材-自定義palette篇》中講解的很詳細(xì)了, 就不做過多的闡述.

同樣是在customModeler/custom的文件夾下創(chuàng)建一個(gè)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中將其導(dǎo)出.

label標(biāo)簽自定義在元素下方

由于評(píng)論區(qū)有小伙伴提了問題: 該如何將label標(biāo)簽自定義在元素的下方?

因此霖呆呆我回去也是花了點(diǎn)時(shí)間研究了一下label標(biāo)簽.

首先label標(biāo)簽實(shí)際上是xml中各個(gè)標(biāo)簽上的一個(gè)名叫name的屬性, 如下圖:

image

開始節(jié)點(diǎn)和lindaidai-task中都有name屬性, 但是在bpmn:StartEvent上能將這個(gè)label顯示出來(lái), 是因?yàn)樵谙旅嬗幸粋€(gè)bpmndi:BPMNLabel的標(biāo)簽.

于是就造成了圖形上是這樣顯示的:

bpmn11.png

那么我們?cè)撊绾螌⑦@里的label顯示出來(lái)呢?

首先讓我們先將Shape打印出來(lái)看看:

bpmn12.png

可以發(fā)現(xiàn)在businessObject中有一個(gè)name屬性...

既然這樣的話, 我們肯定也能在drawShape中拿到這個(gè)name屬性, 之后可以用svgCreate方法給父節(jié)點(diǎn)中添加一個(gè)文本類型的標(biāo)簽.

// 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屬性來(lái)決定是否要渲染出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
}

因?yàn)橛行┰乇旧砭蛶в?code>label屬性的, 比如bpmn:StartEvent, 所以不需要重新渲染, 因此我在util.js中加了一個(gè)hasLabelElements數(shù)組:

// utils/util.js
const hasLabelElements = ['bpmn:StartEvent', 'bpmn:EndEvent'] // 一開始就有l(wèi)abel標(biāo)簽的元素類型

之前我是想通過element.labels.length<=0來(lái)過濾掉開始就有label標(biāo)簽的元素的, 但是發(fā)現(xiàn)在渲染階段還獲取不到labels, 所以長(zhǎng)度一直都會(huì)是0, 就干脆定義一個(gè)hasLabelElements來(lái)判斷好了??...

打開頁(yè)面效果是這樣的:

bpmn13.png

看起來(lái)好像成功了 ! good boy ! ??

但是當(dāng)我雙擊想要去編輯label文字的時(shí)候, 卻出現(xiàn)了這樣的效果:

image

它直接在我原來(lái)圖形的上面新建了一個(gè)輸入框...

額??...其實(shí)我也沒有想到什么好的辦法去解決,在這里我提供一個(gè)看起來(lái)可行的方案:
在雙擊元素的時(shí)候, 將text給移除, 或者將他的innerHTML設(shè)置為''.

當(dāng)然你要是感覺這樣也看得下去的話, 咱不搗鼓也行, 畢竟你編輯這里面的內(nèi)容, 下面的label也是會(huì)同步的變的.

再不濟(jì)的話, 你可以全局修改djs-direct-editing-parent這個(gè)類的樣式, 將下面的文字給覆蓋上也是可以的... 當(dāng)然感覺這個(gè)不是一個(gè)很好的辦法.
app.css中寫入:

.djs-direct-editing-parent {
    top: 130px!important;
    width: 60px!important;
}

總結(jié)

上面的做法主要是利用svgCreate來(lái)創(chuàng)建text元素添加到parentNode中, 其實(shí)bpmn.js中用到了很多ting-svg的東西, 之前也沒接觸過這些, 然后也是通過查找資料了解到svgCreate的用法...

科普一波好了, 哈哈??:
SVG基礎(chǔ)知識(shí)

后語(yǔ)

上面??案例用的都是同一個(gè)項(xiàng)目??

項(xiàng)目案例Git地址: LinDaiDai/bpmn-vue-custom 喜歡的小伙伴請(qǐng)給個(gè)Star??呀, 謝謝??

系列全部目錄請(qǐng)查看此處: 《全網(wǎng)最詳bpmn.js教材》

系列相關(guān)推薦:

《全網(wǎng)最詳bpmn.js教材-基礎(chǔ)篇》

《全網(wǎng)最詳bpmn.js教材-http請(qǐng)求篇》

《全網(wǎng)最詳bpmn.js教材-事件篇》

《全網(wǎng)最詳bpmn.js教材-contextPad篇》

《全網(wǎng)最詳bpmn.js教材-編輯、刪除節(jié)點(diǎn)篇》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容