全網(wǎng)最詳bpmn.js教材-封裝組件篇

前言

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()
    }
}

這樣就可以畫出線了:

bpmnModeler.png

自定義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ù)分為兩類: createActioncreateConnect.

接下來我們只需要構(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
}

這樣看來代碼是不是精簡很多了呢??.

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

bpmnModeler2.png

此時左側(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的一個字符串拼接.

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

效果如下:

bpmnModeler3.png

編寫CustomContextProvider.js代碼

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

bomnModeler4.png

將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教材-http請求篇》

《全網(wǎng)最詳bpmn.js教材-事件篇》

《全網(wǎng)最詳bpmn.js教材-renderer篇》

《全網(wǎng)最詳bpmn.js教材-contextPad篇》

《全網(wǎng)最詳bpmn.js教材-編輯、刪除節(jié)點(diǎn)篇》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末而芥,一起剝皮案震驚了整個濱河市律罢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌棍丐,老刑警劉巖误辑,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異歌逢,居然都是意外死亡巾钉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門秘案,熙熙樓的掌柜王于貴愁眉苦臉地迎上來砰苍,“玉大人潦匈,你說我怎么就攤上這事∽迹” “怎么了茬缩?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長辟癌。 經(jīng)常有香客問我寒屯,道長,這世上最難降的妖魔是什么黍少? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任寡夹,我火速辦了婚禮,結(jié)果婚禮上厂置,老公的妹妹穿的比我還像新娘菩掏。我一直安慰自己,他們只是感情好昵济,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布智绸。 她就那樣靜靜地躺著,像睡著了一般访忿。 火紅的嫁衣襯著肌膚如雪瞧栗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天海铆,我揣著相機(jī)與錄音迹恐,去河邊找鬼。 笑死卧斟,一個胖子當(dāng)著我的面吹牛殴边,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播珍语,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼锤岸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了板乙?” 一聲冷哼從身側(cè)響起是偷,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎亡驰,沒想到半個月后晓猛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡凡辱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年戒职,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片透乾。...
    茶點(diǎn)故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡洪燥,死狀恐怖磕秤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捧韵,我是刑警寧澤市咆,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站再来,受9級特大地震影響蒙兰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜芒篷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一搜变、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧针炉,春花似錦挠他、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至镰烧,卻和暖如春拢军,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背怔鳖。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工朴沿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人败砂。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像魏铅,于是被迫代替她去往敵國和親昌犹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內(nèi)容