全網最詳bpmn.js教材-封裝組件篇

前言

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()
    }
}
複製代碼

這樣就能夠畫出線了:

bpmnModeler.png
bpmnModeler.png

自定義modeler

通過了上面那麼的例子, 其實咱們不難發現, 在每一個關鍵的函數中, 都是將本身想要自定義的東西經過函數返回值傳遞出去.

並且返回值的內容都大同小異, 無非就是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)
       }
     }
   }
 }
複製代碼

所以我這裏把建立元素的函數分爲兩類: createActioncreateConnect.

接下來咱們只須要構建一個這樣的數組:

// 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
}
複製代碼

這樣看來代碼是否是精簡不少了呢😊.

讓咱們來看看頁面的效果:

bpmnModeler2.png
bpmnModeler2.png

此時左側的工具欄就已經所有被替換成咱們想要的圖片了.

編寫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的一個字符串拼接.

這樣的話, 自定義元素就能夠都渲染出來了.

效果以下:

bpmnModeler3.png
bpmnModeler3.png

編寫CustomContextProvider.js代碼

完成了paletterenderer的編寫, 接下來讓咱們看看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就豐富起來了😊.

bomnModeler4.png
bomnModeler4.png

將bpmn封裝成組件

有了自定義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作判斷, 如果不爲空的話則請求獲取數據,不然賦值一個默認值;
  • 初始化成功以後, 在成功的函數中添加 modelerelement的監聽事件;
  • 初始化下載 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教材》

系列相關推薦:

《全網最詳bpmn.js教材-基礎篇》

《全網最詳bpmn.js教材-http請求篇》

《全網最詳bpmn.js教材-事件篇》

《全網最詳bpmn.js教材-renderer篇》

《全網最詳bpmn.js教材-contextPad篇》

《全網最詳bpmn.js教材-編輯、刪除節點篇》

相關文章
相關標籤/搜索