全网最详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教材-编辑、删除节点篇》

相关文章
相关标签/搜索