前言
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)的圖形還是“裸體的”:
這一章節(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)就被渲染成了自定義的“黃金積木”??:
完全自定義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的屬性, 如下圖:
開始節(jié)點(diǎn)和lindaidai-task中都有name屬性, 但是在bpmn:StartEvent上能將這個(gè)label顯示出來(lái), 是因?yàn)樵谙旅嬗幸粋€(gè)bpmndi:BPMNLabel的標(biāo)簽.
于是就造成了圖形上是這樣顯示的:
那么我們?cè)撊绾螌⑦@里的label顯示出來(lái)呢?
首先讓我們先將Shape打印出來(lái)看看:
可以發(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è)面效果是這樣的:
看起來(lái)好像成功了 ! good boy ! ??
但是當(dāng)我雙擊想要去編輯label文字的時(shí)候, 卻出現(xiàn)了這樣的效果:
它直接在我原來(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)求篇》