什么是 bpmn-js ?

一个BPMN 2.0 渲染工具,简单来说,就是流程设计器。前端利用这样的一个流程设计器,设计流程,导出 xml,然后传给后端。启动 activities工作流引擎,最后生成流程实例。 官网地址:https://bpmn.io/toolkit/bpmn-js/ Github地址:https://github.com/bpmn-io

流对象

一个业务流程图有三个流对象的核心元素

  • 事件image.png
    • 一个事件用圆圈来描述,表示一个业务流程期间发生的东西
    • 事件影响流程的流动.一般有一个原因(触发器)或一个影响(结果)
    • 基于它们对流程的影响,有三种事件:开始事件,中间事件,终止事件
  • 活动image.png
    • 用圆角矩形表示,一个流程由一个活动或多个活动组成
  • 条件
    • 条件用菱形表示,用于控制序列流的分支与合并。
    • 可以作为选择,包括路径的分支与合并
    • 内部的标记会给出控制流的类型
  1. 在项目内安装 bpmn-js
    1. npm install bpmn-js --save
  2. 使用 bpmn-js

image.png

palette 左侧工具栏
中间绘画区域
properties-panel 右侧属性栏

使用左侧工具栏, 需要在项目中引用相应的样式:

// 以下为bpmn工作流绘图工具的样式
import 'bpmn-js/dist/assets/diagram-js.css' // 左边工具栏以及编辑节点的样式
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'
import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css' // 右边工具栏样式

开发属性面板,还会涉及到自定义标签的插入人员配置会签属性以及条件表达式等

将基础api , 高级特性刷一遍的

  • 添加自定义元素
  • 自定义调色板/上下文板
  • 自定义形状渲染

项目简介

一个基于 bpmn.js,Vue 2.x 和 ElementUI 开发的流程设计器。 Bpmn Process Designer
支持监听器,扩展属性,表单等配置,可自由扩展

文档说明 Documentation

Attributes

Attribute Description Type Accepted Values Default
value/v-model 初始化流程对应的xml字符串 String - -
translations 翻译文件 Object - zh.js
additionalModel 自定义的附加模块 Object[] / Object - -
moddleExtension 自定义的扩展模块 Object - -
onlyCustomizeAddi 仅使用开发时的自定义附加模块 Boolean - false
onlyCustomizeModdle 仅使用开发时的自定义扩展模块 Boolean - false
prefix 流程引擎对应扩展属性前缀 String camunda,activiti,flowable camunda
events 需要使用的事件列表,可用事件见Bpmn.js 中文文档 Array - [element.click]
headerButtonSize 头部按钮组的大小 String “default”, “medium”, “small”, “mini” small

功能说明

工作流 - 图4

  1. 工具栏:包含常见操作,比如打开文件、下载文件、预览、对齐方式、缩放管理、撤销删除等
  2. 常规信息:id、名称、扩展属性、元素文档
  3. 特殊节点属性:
    1. 流程全局消息与信号
    2. 执行监听器
    3. 用户任务节点 任务监听器
    4. 表单配置
    5. 任务配置
    6. 多实例任务
    7. 流转条件
  4. 内置常用camunda,flowable,activiti解析文件
  5. 自定义左侧元素栏platte与弹出菜单contentPad示例模块
  6. 自定义渲染方法 renderer 模块实例

    通过http请求获取数据并渲染

    监听modeler并绑定事件

    在用户在进行不同操作的时候能够监听到他操作的是什么, 从而做想要做的事情.
    是进行了shape的新增还是进行了线的新增.
    比如如下的一些监听事件:
  • shape.added 新增一个shape之后触发;
  • shape.move.end 移动完一个shape之后触发;
  • shape.removed 删除一个shape之后触发;

并在success()函数中添加上监听事件的函数:

// event.vue
<script>
...
success () {
  this.addModelerListener()
},
// 监听 modeler
addModelerListener() {
  const bpmnjs = this.bpmnModeler
  const that = this
  // 这里我是用了一个forEach给modeler上添加要绑定的事件
  const events = ['shape.added', 'shape.move.end', 'shape.removed', 'connect.end',             'connect.move']
  events.forEach(function(event) {
    that.bpmnModeler.on(event, e => {
      console.log(event, e)
      var elementRegistry = bpmnjs.get('elementRegistry')
      var shape = e.element ? elementRegistry.get(e.element.id) : e.shape
      console.log(shape)
    })
  })
},

监听element并绑定事件

监听用户点击图形上的element或者监听某个element改变:

  • element.click 点击元素;
  • element.changed 当元素发生改变的时候(包括新增、移动、删除元素)

继续在success()上添加监听事件:

// event.vue
<script>
...
success () {
    ...
    this.addEventBusListener()
},
addEventBusListener () {
    let that = this
  const eventBus = this.bpmnModeler.get('eventBus') // 需要使用eventBus
  const eventTypes = ['element.click', 'element.changed'] // 需要监听的事件集合
  eventTypes.forEach(function(eventType) {
    eventBus.on(eventType, function(e) {
      console.log(e)
    })
  })
}
</script>

addEventBusListener()函数后, 在进行元素的点击、新增、移动、删除的时候都能监听到了.
但是有一点很不好, 你在点击“画布”的时候, 也就是根元素也可能会触发此事件, 我们一般都不希望此时会触发, 因此我们可以在on回调中添加一些判断, 来避免掉不需要的情况:

eventBus.on(eventType, function(e) {
  if (!e || e.element.type == 'bpmn:Process') return // 这里我的根元素是bpmn:Process
  console.log(e)
})

通过监听事件判断操作方式

工作中要用到的场景

  • 新增了shape
  • 新增了线(connection)
  • 删除了shape和connection
  • 移动了shape和线 ```javascript
      // event.vue
    
    … success () {
    this.addModelerListener()
    this.addEventBusListener()
    
    }, // 添加绑定事件 addBpmnListener () {
    const that = this
    // 获取a标签dom节点
    const downloadLink = this.$refs.saveDiagram
    const downloadSvgLink = this.$refs.saveSvg
      // 给图绑定事件,当图有发生改变就会触发这个事件
    this.bpmnModeler.on('commandStack.changed', function () {
      that.saveSVG(function(err, svg) {
          that.setEncoded(downloadSvgLink, 'diagram.svg', err ? null : svg)
      })
      that.saveDiagram(function(err, xml) {
          that.setEncoded(downloadLink, 'diagram.bpmn', err ? null : xml)
      })
    })
    
    }, addModelerListener() {
    // 监听 modeler
    const bpmnjs = this.bpmnModeler
    const that = this
    // 'shape.removed', 'connect.end', 'connect.move'
    const events = ['shape.added', 'shape.move.end', 'shape.removed']
    events.forEach(function(event) {
      that.bpmnModeler.on(event, e => {
        var elementRegistry = bpmnjs.get('elementRegistry')
        var shape = e.element ? elementRegistry.get(e.element.id) : e.shape
        // console.log(shape)
        if (event === 'shape.added') {
          console.log('新增了shape')
        } else if (event === 'shape.move.end') {
          console.log('移动了shape')
        } else if (event === 'shape.removed') {
          console.log('删除了shape')
        }
      })
    })
    
    }, addEventBusListener() {
    // 监听 element
    let that = this
    const eventBus = this.bpmnModeler.get('eventBus')
    const eventTypes = ['element.click', 'element.changed']
    eventTypes.forEach(function(eventType) {
      eventBus.on(eventType, function(e) {
        if (!e || e.element.type == 'bpmn:Process') return
        if (eventType === 'element.changed') {
          that.elementChanged(eventType, e)
        } else if (eventType === 'element.click') {
          console.log('点击了element')
        }
      })
    })
    
    }, elementChanged(eventType, e) {
    var shape = this.getShape(e.element.id)
    if (!shape) {
      // 若是shape为null则表示删除, 无论是shape还是connect删除都调用此处
      console.log('无效的shape')
      // 由于上面已经用 shape.removed 检测了shape的删除, 因此这里只判断是否是线
      if (this.isSequenceFlow(shape.type)) {
        console.log('删除了线')
      }
    }
    if (!this.isInvalid(shape.type)) {
      if (this.isSequenceFlow(shape.type)) {
        console.log('改变了线')
      }
    }
    
    }, getShape(id) {
    var elementRegistry = this.bpmnModeler.get('elementRegistry')
    return elementRegistry.get(id)
    
    }, isInvalid (param) { // 判断是否是无效的值
    return param === null || param === undefined || param === ''
    
    }, isSequenceFlow (type) { // 判断是否是线
    return type === 'bpmn:SequenceFlow'
    
    }
** 实际开发过程中,点击某个类型为bpmn:Task的节点的时候, 修改它的name属性,应该会这么做的:**

- 给节点添加点击事件
- 判断节点类型为bpmn:Task , 只对这种类型的节点做后续处理
- 使用updateProperties更新name
```javascript
    createNewDiagram () {
        // 将字符串转换成图显示出来
        this.bpmnModeler.importXML(xmlStr, (err) => {
            if (err) {
                // console.error(err)
            } else {
                // 这里是成功之后的回调, 可以在这里做一系列事情
                this.success()
            }
        })
    },
    success () {
        this.addModelerListener() // 添加监听事件
    },
    addModelerListener () {
            const eventBus = this.bpmnModeler.get('eventBus')
          const modeling = this.bpmnModeler.get('modeling')
          const elementRegistry = this.bpmnModeler.get('elementRegistry');
          const eventTypes = ['element.click', 'element.changed'];
          eventTypes.forEach(function(eventType) {
              eventBus.on(eventType, function (e) {
                  if (!e || !e.element) {
                  console.log('无效的e', e)
                  return
                }
                  if (eventType === 'element.click') {
                  console.log('点击了element', e)
                  var shape = e.element ? elementRegistry.get(e.element.id) : e.shape
                  if (shape.type === 'bpmn:Task') {
                    modeling.updateProperties(shape, {
                        name: '我是修改后的Task名称'
                      })
                  }
                }
            })
        })
    }

当然你也可以一次性修改多个属性

modeling.updateProperties(startEventElement, {
    name: '我是修改后的虚线节点',
    isInterrupting: false
})

通过查找前面的 meta-model descriptor 知道StartEvent还有一个isInterrupting属性, 于是试着修改它, 结果竟然成功了, 将开始节点变为了虚线为边框的节点
工作流 - 图5
bpmnModeler5.png
当然你也可以加一些除了meta-model descriptor里的一些自定义属性:

modeling.updateProperties(shape, {
    name: '我是修改后的虚线节点',
    isInterrupting: false,
    customText: '我是自定义的text属性'
})

自定义Renderer

  • 在默认的Renderer基础上修改
  • 完全自定义Renderer
  • label标签自定义在元素下方

    properties

额外功能

  • 获取流程图最外层的属性(根节点)
    const rootElements = this.bpmnModeler.getDefinitions().rootElements[0];