前言
Q: bpmn.js是什么? ???
bpmn.js是一個BPMN2.0渲染工具包和web建模器, 使得畫流程圖的功能在前端來完成.
Q: 我為什么要寫該系列的教材? ???
因為公司業(yè)務(wù)的需要因而要在項目中使用到bpmn.js
,但是由于bpmn.js
的開發(fā)者是國外友人, 因此國內(nèi)對這方面的教材很少, 也沒有詳細(xì)的文檔. 所以很多使用方式很多坑都得自己去找.在將其琢磨完之后, 決定寫一系列關(guān)于它的教材來幫助更多bpmn.js
的使用者或者是期于找到一種好的繪制流程圖的開發(fā)者. 同時也是自己對其的一種鞏固.
由于是系列的文章, 所以更新的可能會比較頻繁, 您要是無意間刷到了且不是您所需要的還請諒解??.
不求贊??不求心??. 只希望能對你有一點(diǎn)小小的幫助.
封裝組件篇
在進(jìn)入這一章節(jié)的學(xué)習(xí)之前, 我希望你能先掌握前面幾節(jié)的知識點(diǎn): 自定義palette
、自定義renderer
、自定義contextPad
、編輯刪除節(jié)點(diǎn).
因為這一章節(jié)會將前面幾節(jié)的內(nèi)容做一個匯總, 然后提供一個可用的bpmn
組件解決方案.
通過閱讀你可以學(xué)習(xí)到:
創(chuàng)建線節(jié)點(diǎn)
首先讓我們先來了解一下線節(jié)點(diǎn)是如何創(chuàng)建的.
我以CustomPalette.js
為例子??, 還記得在之前講的createTask
嗎, 創(chuàng)建線和它差不多:
// CustomPalette.js
PaletteProvider.$inject = [
...
'globalConnect'
]
PaletteProvider.prototype.getPaletteEntries = function(element) {
const { globalConnect } = this
function createConnect () {
return {
group: 'tools',
className: 'icon-custom icon-custom-flow',
title: '新增線',
action: {
click: function (event) {
globalConnect.toggle(event)
}
}
}
}
return {
'create.lindaidai-task': {...},
'global-connect-tool': createConnect()
}
}
這樣就可以畫出線了:
自定義modeler
經(jīng)過了上面那么的例子, 其實我們不難發(fā)現(xiàn), 在每個關(guān)鍵的函數(shù)中, 都是將自己想要自定義的東西通過函數(shù)返回值傳遞出去.
而且返回值的內(nèi)容都大同小異, 無非就是group栅干、className
等等東西, 那么這樣的話, 我們是不是可以將其整合一下, 減少許多代碼量呢?
我們可以構(gòu)建這樣一個函數(shù):
// CustomPalette.js
function createAction (type, group, className, title, options) {
function createListener (event) {
var shape = elementFactory.createShape(assign({ type }, options))
create.start(event, shape)
}
return {
group,
className,
title: '新增' + title,
action: {
dragstart: createListener,
click: createListener
}
}
}
它接收所有元素不同的屬性, 然后返回一個自定義元素.
但是線的創(chuàng)建可能有些不同:
// CustomPalette.js
function createConnect (type, group, className, title, options) {
return {
group,
className,
title: '新增' + title,
action: {
click: function (event) {
globalConnect.toggle(event)
}
}
}
}
因此我這里把創(chuàng)建元素的函數(shù)分為兩類: createAction
和createConnect
.
接下來我們只需要構(gòu)建一個這樣的數(shù)組:
// utils/util.js
const flowAction = { // 線
type: 'global-connect-tool',
action: ['bpmn:SequenceFlow', 'tools', 'icon-custom icon-custom-flow', '連接線']
}
const customShapeAction = [ // shape
{
type: 'create.start-event',
action: ['bpmn:StartEvent', 'event', 'icon-custom icon-custom-start', '開始節(jié)點(diǎn)']
},
{
type: 'create.end-event',
action: ['bpmn:EndEvent', 'event', 'icon-custom icon-custom-end', '結(jié)束節(jié)點(diǎn)']
},
{
type: 'create.task',
action: ['bpmn:Task', 'activity', 'icon-custom icon-custom-task', '普通任務(wù)']
},
{
type: 'create.businessRule-task',
action: ['bpmn:BusinessRuleTask', 'activity', 'icon-custom icon-custom-businessRule', 'businessRule任務(wù)']
},
{
type: 'create.exclusive-gateway',
action: ['bpmn:ExclusiveGateway', 'activity', 'icon-custom icon-custom-exclusive-gateway', '網(wǎng)關(guān)']
},
{
type: 'create.dataObjectReference',
action: ['bpmn:DataObjectReference', 'activity', 'icon-custom icon-custom-data', '變量']
}
]
const customFlowAction = [
flowAction
]
export { customShapeAction, customFlowAction }
同時構(gòu)建一個方法來循環(huán)創(chuàng)建出上面??的元素:
// utils/util.js
/**
* 循環(huán)創(chuàng)建出一系列的元素
* @param {Array} actions 元素集合
* @param {Object} fn 處理的函數(shù)
*/
export function batchCreateCustom(actions, fn) {
const customs = {}
actions.forEach(item => {
customs[item['type']] = fn(...item['action'])
})
return customs
}
編寫CustomPalette.js
代碼
之后就可以在CustomPalette.js
中來引用它們了:
// CustomPalette.js
import { customShapeAction, customFlowAction, batchCreateCustom } from './../../utils/util'
PaletteProvider.prototype.getPaletteEntries = function(element) {
var actions = {}
const {
create,
elementFactory,
globalConnect
} = this;
function createConnect(type, group, className, title, options) {
return {
group,
className,
title: '新增' + title,
action: {
click: function(event) {
globalConnect.toggle(event)
}
}
}
}
function createAction(type, group, className, title, options) {
function createListener(event) {
var shape = elementFactory.createShape(Object.assign({ type }, options))
create.start(event, shape)
}
return {
group,
className,
title: '新增' + title,
action: {
dragstart: createListener,
click: createListener
}
}
}
Object.assign(actions, {
...batchCreateCustom(customFlowAction, createConnect), // 線
...batchCreateCustom(customShapeAction, createAction)
})
return actions
}
這樣看來代碼是不是精簡很多了呢??.
讓我們來看看頁面的效果:
此時左側(cè)的工具欄就已經(jīng)全部被替換成我們想要的圖片了.
編寫CustomRenderer.js
代碼
然后就到了編寫renderer
代碼的時候了, 在編寫之前, 同樣的, 我們可以做一些配置項.
因為我們注意到在渲染自定義元素的的時候, 靠的就是svgCreate('image', {})
這個方法.
它里面也是接收的一個圖片的地址url
和樣式配置attr
.
那么url
的前綴我們就可以提取出來:
// utils/util.js
const STATICPATH = 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/' // 靜態(tài)文件路徑
const customConfig = { // 自定義元素的配置
'bpmn:StartEvent': {
'field': 'start',
'title': '開始節(jié)點(diǎn)',
'attr': { x: 0, y: 0, width: 40, height: 40 }
},
'bpmn:EndEvent': {
'field': 'end',
'title': '結(jié)束節(jié)點(diǎn)',
'attr': { x: 0, y: 0, width: 40, height: 40 }
},
'bpmn:SequenceFlow': {
'field': 'flow',
'title': '連接線',
},
'bpmn:Task': {
'field': 'rules',
'title': '普通任務(wù)',
'attr': { x: 0, y: 0, width: 48, height: 48 }
},
'bpmn:BusinessRuleTask': {
'field': 'variable',
'title': 'businessRule任務(wù)',
'attr': { x: 0, y: 0, width: 48, height: 48 }
},
'bpmn:ExclusiveGateway': {
'field': 'decision',
'title': '網(wǎng)關(guān)',
'attr': { x: 0, y: 0, width: 48, height: 48 }
},
'bpmn:DataObjectReference': {
'field': 'score',
'title': '變量',
'attr': { x: 0, y: 0, width: 48, height: 48 }
}
}
const hasLabelElements = ['bpmn:StartEvent', 'bpmn:EndEvent', 'bpmn:ExclusiveGateway', 'bpmn:DataObjectReference'] // 一開始就有l(wèi)abel標(biāo)簽的元素類型
export { STATICPATH, customConfig, hasLabelElements }
然后只需要在編寫drawShape
方法的時候判斷一下就可以了:
// CustomRenderer.js
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, STATICPATH, hasLabelElements } from '../../utils/util'
/**
* A renderer that knows how to render custom elements.
*/
export default function CustomRenderer(eventBus, styles, bpmnRenderer) {
BaseRenderer.call(this, eventBus, 2000)
var computeStyle = styles.computeStyle
this.drawElements = function(parentNode, element) {
console.log(element)
const type = element.type // 獲取到類型
if (type !== 'label') {
if (customElements.includes(type)) { // or customConfig[type]
return drawCustomElements(parentNode, element)
}
const shape = bpmnRenderer.drawShape(parentNode, element)
return shape
} else {
element
}
}
}
inherits(CustomRenderer, BaseRenderer)
CustomRenderer.$inject = ['eventBus', 'styles', 'bpmnRenderer']
CustomRenderer.prototype.canRender = function(element) {
// ignore labels
return true
// return !element.labelTarget;
}
CustomRenderer.prototype.drawShape = function(parentNode, element) {
return this.drawElements(parentNode, element)
}
CustomRenderer.prototype.getShapePath = function(shape) {
// console.log(shape)
}
function drawCustomElements(parentNode, element) {
const { type } = element
const { field, attr } = customConfig[type]
const url = `${STATICPATH}${field}.png`
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,
"font-size": "14",
"fill": "#000"
})
text.innerHTML = element.businessObject.name
svgAppend(parentNode, text)
}
return customIcon
}
關(guān)鍵在于drawCustomElements
函數(shù)中, 利用了url
的一個字符串拼接.
這樣的話, 自定義元素就可以都渲染出來了.
效果如下:
編寫CustomContextProvider.js
代碼
完成了palette
和renderer
的編寫, 接下來讓我們看看contextPad
是怎么編寫的.
其實它的寫法和palette
差不多, 只不過有一點(diǎn)需要我們注意的:
不同類型的節(jié)點(diǎn)出現(xiàn)的contextPad
的內(nèi)容可能是不同的.
比如:
-
StartEvent
會出現(xiàn)edit消约、delete、Task延蟹、BusinessRuleTask短条、ExclusiveGateway
等等; -
EndEvent
只能出現(xiàn)edit岩四、delete
; -
SequenceFlow
只能出現(xiàn)edit、delete
.
也就是說我們需要根據(jù)節(jié)點(diǎn)類型來返回不同的contextPad
.
那么在編寫getContextPadEntries
函數(shù)返回值的時候, 就可以根據(jù)element.type
來返回不同的結(jié)果:
import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil'
ContextPadProvider.prototype.getContextPadEntries = function(element) {
... // 此處省略的代碼可查看項目github源碼
// 只有點(diǎn)擊列表中的元素才會產(chǎn)生的元素
if (isAny(businessObject, ['bpmn:StartEvent', 'bpmn:Task', 'bpmn:BusinessRuleTask', 'bpmn:ExclusiveGateway', 'bpmn:DataObjectReference'])) {
Object.assign(actions, {
...batchCreateCustom(customShapeAction, createAction),
...batchCreateCustom(customFlowAction, createConnect), // 連接線
'edit': editElement(),
'delete': deleteElement()
})
}
// 結(jié)束節(jié)點(diǎn)和線只有刪除和編輯
if (isAny(businessObject, ['bpmn:EndEvent', 'bpmn:SequenceFlow', 'bpmn:DataOutputAssociation'])) {
Object.assign(actions, {
'edit': editElement(),
'delete': deleteElement()
})
}
return actions
}
isAny
的作用其實就是判斷類型屬不屬于后面數(shù)組中, 類似于includes
.
這樣我們的contextPad
就豐富起來了??.
將bpmn封裝成組件
有了自定義modeler
的基礎(chǔ), 我們就可以將bpmn
封裝成一個組件, 在我們需要應(yīng)用的地方引用這個組件就可以了.
為了給大家更好演示, 我新建了一個項目 bpmn-custom-modeler , 里面的依賴和配置都和 bpmn-vue-custom中相同, 只不過在這個新的項目里我是打算用自定義的modeler
來覆蓋它原有的, 并封裝一個bpmn
組件來供頁面使用.
前期準(zhǔn)備
在項目的components
文件夾下新建一個名為bpmn
的文件夾, 這里面用來存放封裝的bpmn
組件.
然后我們還可以準(zhǔn)備一個空的xml
作為組件中的默認(rèn)顯示(也就是若是一進(jìn)來沒有任何圖形的時候應(yīng)該顯示的是什么內(nèi)容), 這里我定義了一個newDiagram.js
.
再在根目錄下創(chuàng)建一個views
文件來放一些頁面文件, 這里我就再新建一個custom-modeler.vue
用來引用封裝好的bpmn
組件來看效果.
組件的props
首先讓我們來思考一下, 既然要把它封裝成組件, 那么肯定是需要給這個組件里傳遞props
(可以理解為參數(shù)). 它可以是一整個xml
字符串, 也可以是一個bpmn
文件的地址.
我以傳入bpmn
文件地址為例進(jìn)行封裝. 當(dāng)然你們可以根據(jù)自己的業(yè)務(wù)需求來定.
也就是在引用這個組件的時候, 我期望的是這樣寫:
/* views/custom-modeler.vue */
<template>
<bpmn :xmlUrl="xmlUrl" @change="changeBpmn"></bpmn>
</template>
<script>
import { Bpmn } from './../components/bpmn'
export default {
components: {
Bpmn
},
data () {
return {
xmlUrl: 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmnMock.bpmn'
}
},
methods: {
changeBpmn ($event) {}
}
}
</script>
只要引用了bpmn
組件, 然后傳遞一個url
, 頁面上就可以顯示出對應(yīng)的圖形內(nèi)容.
這樣的話, 我們的Bpmn.vue
中就應(yīng)該這樣定義props
:
// Bpmn.vue
props: {
xmlUrl: {
type: String,
default: ''
}
}
編寫組件的hmtl
代碼
組件中的html
代碼十分容易, 主要是給畫布一個盛放的容器, 再定義了兩個按鈕用于下載:
<!-- Bpmn.vue -->
<template>
<div class="containers">
<div class="canvas" ref="canvas"></div>
<div id="js-properties-panel" class="panel"></div>
<ul class="buttons">
<li>
<a ref="saveDiagram" href="javascript:" title="保存為bpmn">保存為bpmn</a>
</li>
<li>
<a ref="saveSvg" href="javascript:" title="保存為svg">保存為svg</a>
</li>
</ul>
</div>
</template>
編寫組件的js
代碼
在js
里, 我就將前面幾節(jié)《全網(wǎng)最詳bpmn.js教材-http請求篇》 和 《全網(wǎng)最詳bpmn.js教材-http事件篇》
中的功能都整合了進(jìn)來.
大體就是:
- 初始化的時候, 對輸入進(jìn)來的
xmlUrl
做判斷, 若是不為空的話則請求獲取數(shù)據(jù),否則賦值一個默認(rèn)值; - 初始化成功之后, 在成功的函數(shù)中添加
modeler
屏镊、element
的監(jiān)聽事件; - 初始化下載
xml依疼、svg
的鏈接按鈕.
例如:
// Bpmn.vue
async createNewDiagram () {
const that = this
let bpmnXmlStr = ''
if (this.xmlUrl === '') { // 判斷是否存在
bpmnXmlStr = this.defaultXmlStr
this.transformCanvas(bpmnXmlStr)
} else {
let res = await axios({
method: 'get',
timeout: 120000,
url: that.xmlUrl,
headers: { 'Content-Type': 'multipart/form-data' }
})
console.log(res)
bpmnXmlStr = res['data']
this.transformCanvas(bpmnXmlStr)
}
},
transformCanvas(bpmnXmlStr) {
// 將字符串轉(zhuǎn)換成圖顯示出來
this.bpmnModeler.importXML(bpmnXmlStr, (err) => {
if (err) {
console.error(err)
} else {
this.success()
}
// 讓圖能自適應(yīng)屏幕
var canvas = this.bpmnModeler.get('canvas')
canvas.zoom('fit-viewport')
})
},
success () {
this.addBpmnListener()
this.addModelerListener()
this.addEventBusListener()
},
addBpmnListener () {},
addModelerListener () {},
addEventBusListener () {}
整合之后的代碼有些多, 這里貼出來有點(diǎn)不太好, 詳細(xì)代碼在gitHub
上有: LinDaiDai/bpmn-custom-modeler/Bpmn.vue
后語
項目案例Git地址: LinDaiDai/bpmn-vue-custom 喜歡的小伙伴請給個Star
??呀, 謝謝??
系列全部目錄請查看此處: 《全網(wǎng)最詳bpmn.js教材》
系列相關(guān)推薦:
《全網(wǎng)最詳bpmn.js教材-基礎(chǔ)篇》
《全網(wǎng)最詳bpmn.js教材-renderer篇》