Q: bpmn.js是什麼? 🤔️javascript
」
bpmn.js是一個BPMN2.0渲染工具包和web建模器, 使得畫流程圖的功能在前端來完成.html
Q: 我爲何要寫該系列的教材? 🤔️前端
」
由於公司業務的須要於是要在項目中使用到bpmn.js
,可是因爲bpmn.js
的開發者是國外友人, 所以國內對這方面的教材不多, 也沒有詳細的文檔. 因此不少使用方式不少坑都得本身去找.在將其琢磨完以後, 決定寫一系列關於它的教材來幫助更多bpmn.js
的使用者或者是期於找到一種好的繪製流程圖的開發者. 同時也是本身對其的一種鞏固.vue
因爲是系列的文章, 因此更新的可能會比較頻繁, 您要是無心間刷到了且不是您所須要的還請諒解😊.java
不求贊👍不求心❤️. 只但願能對你有一點小小的幫助.ios
在進入這一章節的學習以前, 我但願你能先掌握前面幾節的知識點: 自定義palette
、自定義renderer
、自定義contextPad
、編輯刪除節點.git
由於這一章節會將前面幾節的內容作一個彙總, 而後提供一個可用的bpmn
組件解決方案.github
經過閱讀你能夠學習到:web
首先讓咱們先來了解一下線節點是如何建立的.canvas
我以CustomPalette.js
爲例子🌰, 還記得在以前講的createTask
嗎, 建立線和它差很少:
// 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()
}
}
複製代碼
這樣就能夠畫出線了:
通過了上面那麼的例子, 其實咱們不難發現, 在每一個關鍵的函數中, 都是將本身想要自定義的東西經過函數返回值傳遞出去.
並且返回值的內容都大同小異, 無非就是group、className
等等東西, 那麼這樣的話, 咱們是否是能夠將其整合一下, 減小許多代碼量呢?
咱們能夠構建這樣一個函數:
// 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
}
}
}
複製代碼
它接收全部元素不一樣的屬性, 而後返回一個自定義元素.
可是線的建立可能有些不一樣:
// CustomPalette.js
function createConnect (type, group, className, title, options) {
return {
group,
className,
title: '新增' + title,
action: {
click: function (event) {
globalConnect.toggle(event)
}
}
}
}
複製代碼
所以我這裏把建立元素的函數分爲兩類: createAction
和createConnect
.
接下來咱們只須要構建一個這樣的數組:
// 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', '開始節點']
},
{
type: 'create.end-event',
action: ['bpmn:EndEvent', 'event', 'icon-custom icon-custom-end', '結束節點']
},
{
type: 'create.task',
action: ['bpmn:Task', 'activity', 'icon-custom icon-custom-task', '普通任務']
},
{
type: 'create.businessRule-task',
action: ['bpmn:BusinessRuleTask', 'activity', 'icon-custom icon-custom-businessRule', 'businessRule任務']
},
{
type: 'create.exclusive-gateway',
action: ['bpmn:ExclusiveGateway', 'activity', 'icon-custom icon-custom-exclusive-gateway', '網關']
},
{
type: 'create.dataObjectReference',
action: ['bpmn:DataObjectReference', 'activity', 'icon-custom icon-custom-data', '變量']
}
]
const customFlowAction = [
flowAction
]
export { customShapeAction, customFlowAction }
複製代碼
同時構建一個方法來循環建立出上面👆的元素:
// utils/util.js
/** * 循環建立出一系列的元素 * @param {Array} actions 元素集合 * @param {Object} fn 處理的函數 */
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
}
複製代碼
這樣看來代碼是否是精簡不少了呢😊.
讓咱們來看看頁面的效果:
此時左側的工具欄就已經所有被替換成咱們想要的圖片了.
CustomRenderer.js
代碼而後就到了編寫renderer
代碼的時候了, 在編寫以前, 一樣的, 咱們能夠作一些配置項.
由於咱們注意到在渲染自定義元素的的時候, 靠的就是svgCreate('image', {})
這個方法.
它裏面也是接收的一個圖片的地址url
和樣式配置attr
.
那麼url
的前綴咱們就能夠提取出來:
// utils/util.js
const STATICPATH = 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/' // 靜態文件路徑
const customConfig = { // 自定義元素的配置
'bpmn:StartEvent': {
'field': 'start',
'title': '開始節點',
'attr': { x: 0, y: 0, width: 40, height: 40 }
},
'bpmn:EndEvent': {
'field': 'end',
'title': '結束節點',
'attr': { x: 0, y: 0, width: 40, height: 40 }
},
'bpmn:SequenceFlow': {
'field': 'flow',
'title': '鏈接線',
},
'bpmn:Task': {
'field': 'rules',
'title': '普通任務',
'attr': { x: 0, y: 0, width: 48, height: 48 }
},
'bpmn:BusinessRuleTask': {
'field': 'variable',
'title': 'businessRule任務',
'attr': { x: 0, y: 0, width: 48, height: 48 }
},
'bpmn:ExclusiveGateway': {
'field': 'decision',
'title': '網關',
'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'] // 一開始就有label標籤的元素類型
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
}
複製代碼
關鍵在於drawCustomElements
函數中, 利用了url
的一個字符串拼接.
這樣的話, 自定義元素就能夠都渲染出來了.
效果以下:
CustomContextProvider.js
代碼完成了palette
和renderer
的編寫, 接下來讓咱們看看contextPad
是怎麼編寫的.
其實它的寫法和palette
差很少, 只不過有一點須要咱們注意的:
不一樣類型的節點出現的contextPad
的內容多是不一樣的.
好比:
StartEvent
會出現
edit、delete、Task、BusinessRuleTask、ExclusiveGateway
等等;
EndEvent
只能出現
edit、delete
;
SequenceFlow
只能出現
edit、delete
.
也就是說咱們須要根據節點類型來返回不一樣的contextPad
.
那麼在編寫getContextPadEntries
函數返回值的時候, 就能夠根據element.type
來返回不一樣的結果:
import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil'
ContextPadProvider.prototype.getContextPadEntries = function(element) {
... // 此處省略的代碼可查看項目github源碼
// 只有點擊列表中的元素纔會產生的元素
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()
})
}
// 結束節點和線只有刪除和編輯
if (isAny(businessObject, ['bpmn:EndEvent', 'bpmn:SequenceFlow', 'bpmn:DataOutputAssociation'])) {
Object.assign(actions, {
'edit': editElement(),
'delete': deleteElement()
})
}
return actions
}
複製代碼
isAny
的做用其實就是判斷類型屬不屬於後面數組中, 相似於includes
.
這樣咱們的contextPad
就豐富起來了😊.
有了自定義modeler
的基礎, 咱們就能夠將bpmn
封裝成一個組件, 在咱們須要應用的地方引用這個組件就能夠了.
爲了給你們更好演示, 我新建了一個項目 bpmn-custom-modeler , 裏面的依賴和配置都和 bpmn-vue-custom中相同, 只不過在這個新的項目裏我是打算用自定義的modeler
來覆蓋它原有的, 並封裝一個bpmn
組件來供頁面使用.
在項目的components
文件夾下新建一個名爲bpmn
的文件夾, 這裏面用來存放封裝的bpmn
組件.
而後咱們還能夠準備一個空的xml
做爲組件中的默認顯示(也就是如果一進來沒有任何圖形的時候應該顯示的是什麼內容), 這裏我定義了一個newDiagram.js
.
再在根目錄下建立一個views
文件來放一些頁面文件, 這裏我就再新建一個custom-modeler.vue
用來引用封裝好的bpmn
組件來看效果.
props
首先讓咱們來思考一下, 既然要把它封裝成組件, 那麼確定是須要給這個組件裏傳遞props
(能夠理解爲參數). 它能夠是一整個xml
字符串, 也能夠是一個bpmn
文件的地址.
我以傳入bpmn
文件地址爲例進行封裝. 固然大家能夠根據本身的業務需求來定.
也就是在引用這個組件的時候, 我指望的是這樣寫:
/* 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
, 頁面上就能夠顯示出對應的圖形內容.
這樣的話, 咱們的Bpmn.vue
中就應該這樣定義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
裏, 我就將前面幾節《全網最詳bpmn.js教材-http請求篇》 和 《全網最詳bpmn.js教材-http事件篇》 中的功能都整合了進來.
大致就是:
xmlUrl
作判斷, 如果不爲空的話則請求獲取數據,不然賦值一個默認值;
modeler
、
element
的監聽事件;
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) {
// 將字符串轉換成圖顯示出來
this.bpmnModeler.importXML(bpmnXmlStr, (err) => {
if (err) {
console.error(err)
} else {
this.success()
}
// 讓圖能自適應屏幕
var canvas = this.bpmnModeler.get('canvas')
canvas.zoom('fit-viewport')
})
},
success () {
this.addBpmnListener()
this.addModelerListener()
this.addEventBusListener()
},
addBpmnListener () {},
addModelerListener () {},
addEventBusListener () {}
複製代碼
整合以後的代碼有些多, 這裏貼出來有點不太好, 詳細代碼在gitHub
上有: LinDaiDai/bpmn-custom-modeler/Bpmn.vue
項目案例Git地址: LinDaiDai/bpmn-custom-modeler 喜歡的小夥伴請給個Star
🌟呀, 謝謝😊
系列所有目錄請查看此處: 《全網最詳bpmn.js教材》
系列相關推薦: