全網最詳bpmn.js教材自定義renderer篇

NO IMAGE

前言

Q: bpmn.js是什麼? 🤔️

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

Q: 我為什麼要寫該系列的教材? 🤔️

因為公司業務的需要因而要在項目中使用到bpmn.js,但是由於bpmn.js的開發者是國外友人, 因此國內對這方面的教材很少, 也沒有詳細的文檔. 所以很多使用方式很多坑都得自己去找.在將其琢磨完之後, 決定寫一系列關於它的教材來幫助更多bpmn.js的使用者或者是期於找到一種好的繪製流程圖的開發者. 同時也是自己對其的一種鞏固.

由於是系列的文章, 所以更新的可能會比較頻繁, 您要是無意間刷到了且不是您所需要的還請諒解😊.

不求贊👍不求心❤️. 只希望能對你有一點小小的幫助.

自定義Renderer篇

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

但是同時我們也知道僅僅只改變Palette是不夠的, 因為繪畫出來的圖形還是“裸體的”:

全網最詳bpmn.js教材自定義renderer篇

這一章節我們就來看一下如何自定義畫布上的圖形, 也就是實現自定義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的節點就被渲染成了自定義的“黃金積木”😝:
全網最詳bpmn.js教材自定義renderer篇

完全自定義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的屬性, 如下圖:

全網最詳bpmn.js教材自定義renderer篇

開始節點和lindaidai-task中都有name屬性, 但是在bpmn:StartEvent上能將這個label顯示出來, 是因為在下面有一個bpmndi:BPMNLabel的標籤.

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

全網最詳bpmn.js教材自定義renderer篇
bpmn11.png

那麼我們該如何將這裡的label顯示出來呢?

首先讓我們先將Shape打印出來看看:
全網最詳bpmn.js教材自定義renderer篇

可以發現在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來判斷好了😓…

打開頁面效果是這樣的:

全網最詳bpmn.js教材自定義renderer篇
bpmn13.png

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

但是當我雙擊想要去編輯label文字的時候, 卻出現了這樣的效果:

全網最詳bpmn.js教材自定義renderer篇

它直接在我原來圖形的上面新建了一個輸入框…

額😅…其實我也沒有想到什麼好的辦法去解決,在這裡我提供一個看起來可行的方案:
在雙擊元素的時候, 將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教材-編輯、刪除節點篇》

相關文章

網頁使用外鏈圖片403無法顯示問題

Filddler抓包

小程序雲開發(實現網頁端)

java入門002~jdk8window版32位64位Mac版64位安裝包