前言

本文以2020年10月为时间节点,功能早就做了,但文章一直没有写

研究过程

根据项目需求,需要实现一个工作流/流程图设计器,并且可配置流转、活动节点、流程的各项属性,也是研究了多个方案

自研方案
使用svg方案(使用 svg.js库)来实现流程图的绘制,图形的拖拽、旋转、缩放、属性面板基本都实现了,但是无法解决连接线绘制的功能,后面尝试使用d3和jsplumb库来解决连接线的绘制,效果不是很好,研发时间给的不够、人手也不够,最后就放弃了

  • svg.js+d3.js
  • svg.js+jsplumb.js

半成品界面如下:
在这里插入图片描述

开源方案

  • VFD: 这应该是我找到的第一个工作流设计器成品,基于jsplumb开发,但是封装的不太好,逻辑都在vue组件中,最后没有用此方案
  • svgedit: 一个svg编辑器,本来打算在此基础上二次开发,但是没文档,代码量还有点大,研究了几天没找到入口点,最后放弃此方案
  • flowchart : 一位个人作者基于d3.js实现的,长时间没有维护了,不满足需求
  • antv-xflow: 依稀记得当时是在ant-design官网中看到的一个组件例子,当时还没有这么强大,也不知道叫这个名字,所以也没有仔细研究,最近写文章的时候看了下demo,感觉不错,貌似只支持react不支持vue?
  • bpmn-js: 这个库在最开始研究的时候就看到过,看了demo觉得功能比较完善,因为几乎没有文档被我忽略了,在一次交流中后端同事说准备采用activiticamunda这两个开源流程引擎做参考来实现自研工作流引擎(这两个引擎的前端都是基于bpmn-js开发),最后还是硬着头皮使用这个方案了
  • bpmn-process-designer:我之前基于bpmn-js开发好的设计器1.0版本ui不太好看,这是今年在开发2.0版本的时候才发现的,一个基于bpmn-js+element-ui实现的工作流设计器,做的挺好的,有参考它界面来做2.0版本

最终成品界面如下
1.0:
在这里插入图片描述
2.0:
在这里插入图片描述

直接使用现有开源引擎
没有过多了解,只举例一些比较出名的开源库,听后端同事说这几个库大同小异

bpmn-js是什么

  • bpmn-js是一个基于bpmn规范的流程图设计器js库,在diagram-js库的基础上开发(图片来源:PL-FE
    在这里插入图片描述

  • bpmn-js官网基本没有提供文档,只有官方例子,所以学习起来比较吃力:

  • 这里推荐这位程序媛写的的教程bpmn-chinese-document,写的比较详细的,也感谢这位大佬的总结和分享

  • 本文使用到各库的版本为:bpmn-js@8.8.1,bpmn-js-properties-panel@0.46.0,camunda-bpmn-moddle@6.1.2

  • 相关代码上传到了gitee

基础使用

安装

npm install bpmn-js@8.8.1 -S

bpmn库导出模块

  • Viewer(lib/Viewer): BPMN 图表查看器,功能简单,只能用于展示
  • NavigatedViewer(lib/NavigatedViewer): BPMN 图表导航查看器,继承Viewer ,包含MoveCanvasModule(鼠标导航)、KeyboardMoveModule(键盘导航)、ZoomScrollModule(缩放滚动)工具的图表查看器
  • Modeler(lib/Modeler): BPMN 图表建模器,融合Viewer、NavigatedViewer类,有元素对齐、工具栏、属性面板等,实现建模能力

BPMN2.0规范的xml结构

在这里插入图片描述
上面所展示的流程图,其xml结构如下:

<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1oszlza" targetNamespace="http://bpmn.io/schema/bpmn">
  <bpmn:process id="Process_0qbzwnl" name="测试" isExecutable="true">
    <bpmn:startEvent id="StartEvent_1142pjw" name="开始">
      <bpmn:outgoing>Flow_0udz675</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:task id="Activity_1umsb7z" name="审批">
      <bpmn:incoming>Flow_0udz675</bpmn:incoming>
      <bpmn:outgoing>Flow_1fwu6d6</bpmn:outgoing>
    </bpmn:task>
    <bpmn:sequenceFlow id="Flow_0udz675" sourceRef="StartEvent_1142pjw" targetRef="Activity_1umsb7z" />
    <bpmn:task id="Activity_11qrnub" name="执行">
      <bpmn:incoming>Flow_1fwu6d6</bpmn:incoming>
      <bpmn:outgoing>Flow_0gs9y2g</bpmn:outgoing>
    </bpmn:task>
    <bpmn:sequenceFlow id="Flow_1fwu6d6" sourceRef="Activity_1umsb7z" targetRef="Activity_11qrnub" />
    <bpmn:endEvent id="Event_09ptfxq" name="结束">
      <bpmn:incoming>Flow_0gs9y2g</bpmn:incoming>
    </bpmn:endEvent>
    <bpmn:sequenceFlow id="Flow_0gs9y2g" sourceRef="Activity_11qrnub" targetRef="Event_09ptfxq" />
  </bpmn:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0qbzwnl">
      <bpmndi:BPMNEdge id="Flow_0udz675_di" bpmnElement="Flow_0udz675">
        <di:waypoint x="209" y="120" />
        <di:waypoint x="260" y="120" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_1fwu6d6_di" bpmnElement="Flow_1fwu6d6">
        <di:waypoint x="360" y="120" />
        <di:waypoint x="420" y="120" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0gs9y2g_di" bpmnElement="Flow_0gs9y2g">
        <di:waypoint x="520" y="120" />
        <di:waypoint x="582" y="120" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="StartEvent_1142pjw_di" bpmnElement="StartEvent_1142pjw">
        <dc:Bounds x="173" y="102" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="180" y="145" width="22" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_1umsb7z_di" bpmnElement="Activity_1umsb7z">
        <dc:Bounds x="260" y="80" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_11qrnub_di" bpmnElement="Activity_11qrnub">
        <dc:Bounds x="420" y="80" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_09ptfxq_di" bpmnElement="Event_09ptfxq">
        <dc:Bounds x="582" y="102" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="589" y="145" width="22" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn:definitions>

创建一个流程图实例

可以使用createDiagram方法创建一个流程图

import Modeler from 'bpmn-js/lib/Modeler'

let bpmnModeler = new Modeler({
  container: "#bpmn-canvas",
})

bpmnModeler.createDiagram()

但以上方式存在一个缺陷,createDiagram方式实际是调用了importXML传入一个常量xml字符串,会导致元素ID重复,源码截图如下:
在这里插入图片描述在这里插入图片描述

优化后的方法如下,仅供参考:


function createDiagram(bpmnModeler, processId) {
  let moddle = bpmnModeler.get('moddle'),
    processId = moddle.ids.next(),
    startEventId = moddle.ids.next()
  return bpmnModeler.importXML(`<?xml version="1.0" encoding="UTF-8"?>
      <bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
                        xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" 
                        xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" 
                        xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" 
                        targetNamespace="http://bpmn.io/schema/bpmn" 
                        id="Definitions_${moddle.ids.next()}">
        <bpmn:process id="Process_${processId}" isExecutable="true">
          <bpmn:startEvent id="StartEvent_${startEventId}"/>
        </bpmn:process>
        <bpmndi:BPMNDiagram id="BPMNDiagram_1">
          <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_${processId}">
            <bpmndi:BPMNShape id="StartEvent_${startEventId}_di" bpmnElement="StartEvent_${startEventId}">
              <dc:Bounds height="36.0" width="36.0" x="173.0" y="102.0"/>
            </bpmndi:BPMNShape>
          </bpmndi:BPMNPlane>
        </bpmndi:BPMNDiagram>
      </bpmn:definitions>`)
}

界面布局结构

  • palette(工具栏) :提供拖拽工具、框选工具、连线工具、基本图元等
  • contextPad(上下文面板):可以理解为快捷面板
  • propertiesPanel(属性面板):定义流程图中图形元素属性
  • shape(图形) 是所有图形的基类(比如Connection,Root)

在这里插入图片描述
图片来源:PL-FE

导入与导出

导入

// 异步方式(推荐)
let result = await bpmnModeler.importXML(xml)

// 回调方式
bpmnModeler.importXML(xml, (result) => {} )

导入的生命周期事件如下:

  • import.parse.start (即将从xml读取模型)
  • import.parse.complete (模型读取完成)
  • import.render.start (图形导入开始)
  • import.render.complete (图形导入完成)
  • import.done (一切都完成)

然而在import.done之后还会触发shape.added,我觉得这是一个bug,后续会讲到如何处理

导出

  • 导出xml
// 异步方式
let { xml } = await bpmnModeler.saveXML()

// 回调方式
bpmnModeler.saveXML({ format: false },({ xml }) => {})

// 格式化导出的xml
let { xml } = await bpmnModeler.saveXML({ format: true })

  • 导出svg
// 异步方式
let { svg } = await bpmnModeler.saveSVG()

// 回调方式
bpmnModeler.saveXML(( { svg } )=>{ })




内部模块/供应商/服务

diagram-js提供了一个get方法来获取器内部模块(有些文章叫核心服务、其内部方法定义英文为_providers),简单介绍一下几个常用模块经常用到的方法

  • 获取一个模块
// 第一个参数为模块名称,第二参数表示是否严格模式
bpmnModeler.get("模块名称",false)
  • eventBus - 事件总线,管理bpmn实例中所有事件
  • canvas - 画布,管理svg元素、连线/图形的添加/删除、缩放等
  • commandStack - 命令堆栈,管理bpmn内部所有命令操作,提供撤销、重做功能等
  • elementRegistry - 元素注册表,管理bpmn内部所有元素
  • moddle - 模型管理,用于管理bpmn的xml结构
  • modeling - 建模器,绘图时用到,提供用于更新画布上元素的 API(移动、删除)

事件总线 - eventBus

  • 获取事件总线模块
let eventBus = bpmnModeler.get("eventBus")
  • 监听事件
// 监听事件
eventBus.on('element.changed', (ev) => {})

// 监听多个事件
eventBus.on(
 ['shape.added', 'connection.added', 'shape.removed', 'connection.removed'],
 (ev) => { 
 }
)

// 设置优先级
eventBus.on('element.changed', 100, (ev) => {})

// 传入上下文
eventBus.on('element.changed', (ev) => {}, that)

// 使用所有参数
eventBus.on('事件名称', 优先级(可选), 回调函数, 上下文(可选))
  • 只监听一次事件
// 用法同on
eventBus.once('事件名称', 优先级(可选), 回调函数, 上下文(可选))
  • 取消监听事件
// 取消监听
eventBus.off('element.changed', callback)

// 取消监听多个事件
eventBus.off(['shape.added', 'connection.added', 'shape.removed', 'connection.removed'], callback)
  • 触发事件
eventBus.fire('element.changed', data)

bpmn内部事件非常之多,我这里举例几个常用事件:

  • 导入导出相关
    ‘import.parse.start’
    ‘import.parse.complete’
    ‘import.render.start’
    ‘import.render.complete’
    ‘import.done’
    ‘saveXML.start’
    ‘saveXML.serialized’
    ‘saveXML.done’
    ‘saveSVG.start’
    ‘saveSVG.done’

  • 画布相关
    ‘canvas.destroy’
    ‘canvas.init’
    ‘canvas.resized’
    ‘canvas.viewbox.changed’
    ‘canvas.viewbox.changing’

  • 图形相关
    ‘shape.added’
    ‘shape.changed’
    ‘shape.remove’
    ‘shape.removed’
    ‘connection.added’
    ‘connection.changed’
    ‘connection.remove’
    ‘connection.removed’

  • 元素相关
    ‘element.changed’
    ‘element.click’
    ‘element.dblclick’
    ‘element.hover’
    ‘element.mousedown’
    ‘element.mousemove’
    ‘element.updateId’

  • 选集相关
    selection.changed 当前选集改变(实际上,每次鼠标点击都会触发)

具体类型定义见此

画布 - canvas

  • 获取画布模块
let canvas = bpmnModeler.get("canvas")
  • 缩放
/**
 *
 * @param {'fit-viewport' | 'fit-content' | number} lvl
 * @param {'auto'|{ x: number, y: number }} center
 */
function zoom(lvl, center) {
  let canvas = bpmnModeler.get('canvas')
  canvas.zoom(lvl, center)
}

// 适应容器缩放
zoom('fit-canvas','auto')

// 完全显示内容
zoom('fit-content','auto')
  • 对齐(选择多个元素使用shift+鼠标左键)
/**
 * 获取当前选集并对齐
 * @param {'left'|'right'|'top'|'bottom'|'middle'|'center'} mode
 */
function align(mode) {
  const align = bpmnModeler.get('alignElements')
  const selection = bpmnModeler.get('selection')
  const elements = selection.get()
  if (!elements || elements.length === 0) {
    return
  }

  align.trigger(elements, mode)
}

具体类型定义见此

命令堆栈 - commandStack

  • 获取命令堆栈模块
let commandStack = bpmnModeler.get('commandStack')
  • 重做、撤销
// 是否可以重做 
let canRedo = commandStack.canRedo()

// 是否可以撤销
let canUndo= commandStack.canUndo()

// 撤销
commandStack.undo()

// 重做 
commandStack.redo()

  • 获取堆栈当前位置
let index = commandStack._stackIdx
  • 清空堆栈
commandStack.clear()

具体类型定义见此

元素注册表 - elementRegistry

  • 获取元素注册表模块
let elementRegistry = bpmnModeler.get('elementRegistry')
  • 遍历所有元素
elementRegistry.forEach((shape, svgElement) => { })
  • 获取指定元素
let shape = elementRegistry.get(元素id或者SVGElement)
  • 获取过滤后的元素
let shapes = elementRegistry.filter((shape) => shape.type === 'bpmn:Task')
  • 更新元素ID
elementRegistry.updateId(shape, "123xxxxsssd")
  • 删除一个元素
elementRegistry .remove(传入SVGElement)

具体类型定义见此

模型 - moddle

基本上没有用到,具体类型定义见此

建模器 - modeling

  • 获取建模器模块
let modeling= bpmnModeler.get('modeling')
  • 修改元素显示文本(常用)
modeling.updateLabel(shape, '审核')
  • 修改元素属性(常用)
modeling.updateProperties(shape, { 属性名称: 属性值 })
  • 对齐元素集合
const selection = bpmnModeler.get('selection')
const elements = selection.get()
modeling.updateProperties(selection, 'left')

具体类型定义见此

属性面板

流程图的绘制基本实现了,但一个完整的工作流还需要在其流程、每个节点、流转上配置一些属性,比如审核规则、参与者、流程名称、变量等,属性面板的作用就是如此。

官方属性面板

如果你想使用官方提供的属性面板,需要先安装两个插件:

npm install bpmn-js-properties-panel@0.46.0 --S
npm install camunda-bpmn-moddle@6.1.2 --S

在创建实例时导入模块

import BpmnModeler from 'bpmn-js/lib/Modeler'
import propertiesPanelModule from 'bpmn-js-properties-panel'
// camunda提供的属性(一般用这个)
import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda'
// bpmn原生属性
// import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/bpmn'

import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda'

let bpmnModeler= new BpmnModeler({
  additionalModules: [propertiesPanelModule, propertiesProviderModule],
  container: '#canvas',
  propertiesPanel: {
    parent: '#properties'
  },
  moddleExtensions: {
    camunda: camundaModdleDescriptor
  }
})

界面如下(经过汉化,后面会介绍)
在这里插入图片描述
随便输入几个属性值,调用saveXML,可以发现保存到了xml中
在这里插入图片描述
如果你想要基于bpmn-js-properties-panel的实现自定义属性面板,请记住modeling.updateProperties这个方法,一定会用到,可参考以下文章、例子:

自定义属性面板

工作流设计器最重要的就是属性面板了,在绘制阶段要针对流转、活动节点要配置不同的属性,以支持后端工作流引擎的运行工作流实例。
最开始是想使用现有的东西来实现,即bpmn官方提供的bpmn-js-properties-panel,学习后发现有以下缺点:

  1. 没有文档,只有一些例子,并且自定义属性的例子看不懂
  2. 不知道如何自定义下拉框组件的数据来源,也不知道如何自定义输入器
  3. 配置的属性会放到bpmn的xml中,然而后端用不到读取也麻烦,前端保存时提取属性数据出来保存也比较麻烦(除非后端拉取camunda分支来实现工作流引擎,这样就可以通过camunda解析)

最终决定自行开发一个属性面板组件,考虑到易维护性,采用了属性描述数据模式来实现面板的渲染,这样各个业务系统要使用到工作流设计器的话,只需要针对业务来配置属性即可,降低了维护成本,
由于自定义属性面板组件跟项目公共库有些耦合,在这里我只提供开发思路,就不放源码了

自定义属性面板组件目录结构如下:
在这里插入图片描述
目前支持的输入器类型有:

  • string:el-input
  • checkbox-group:el-radio-group+el-checkbox-group
  • select\enum:uiot-tree-select
  • 以下未经测试过
  • boolean:el-switch
  • date:el-date-picker
  • time:el-time-picker
  • color:el-color-picker
  • number:el-input-number
  • custom: 自定义输入器组件

属性描述定义数据是一个三级树结构,分为tab、group、prop:

export default {
  name: 'general',
  label: '常规',
  groups: [
    {
      name: 'base',
      label: '基础',
      props: [
        {
          name: 'element-id',
          field: 'id',
          hidden: true,
          type: 'string',
          get: function ({ propsData, bpmn, element } = {}) {
            return element.id
          }
        },
        {
          name: 'process-name',
          field: 'name',
          label: '名称',
          type: 'string',
          required: true,
          supportNodes: ['bpmn:Process']
        }
      ]
    }
  ]
}

其中定义的set、get方法的作用是为了获取\设置bpmn元素中已有属性,会通过Object.defineProperty方法将指定属性定义到业务数据中
在这里插入图片描述

PropTabDefine、PropGroupDefine、PropItemDefine对象定义如下图所示:
在这里插入图片描述

主要是通过监听‘import.done’,‘shape.added’, ‘connection.added’, ‘shape.removed’, 'connection.removed’四个事件来构建每一个活动节点、流转的业务属性数据
在这里插入图片描述

在这里插入图片描述
实际使用与界面展示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

高级使用/优化扩展

完善类型推断

bpmn-js没有提供ts类型定义文件,导致无法使用类型推断,又没有官方文档,无法知道其提供的类、模块有哪些方法、属性,每次想用到啥功能的时候只能去百度,然后到源码中全局搜索,我这里看源码总结了一些d.ts文件,欢迎各位一起维护,具体见此
在这里插入图片描述
在这里插入图片描述

启用快捷键功能

使用bpmn-js官方demo你会发现,bpmn-js是支持快捷键的
在这里插入图片描述
不过在开发过程中发现按下键盘无论如何都没法触发内部的快捷键,在容器的div上加tabindex也没用,去看官方的例子,也没找到相关内容,最后让我扒官方demo的源码扒到了,官方是完全没介绍这个参数(这个参数在在diagram-js源码的lib\features\keyboard\Keyboard.js 69行处被使用)

const container =  document.getElementById('bpmn-container')
 // 创建实例传入opions时,加上keyboard这个属性
let bpmnModeler = new BpmnModeler({
  container: container,
  keyboard: {
    bindTo: container // 绑定到哪个元素上(按键事件的目标元素)
  }, 
});

在这里插入图片描述

控制Viewer可缩放、可拖动

当只需要预览一个流程图时,会用到bpmn-js提供的NavigatedViewer类,某些场景下我们需要控制拖拽和缩放这两个功能的启用和禁用,可通过监听element.mousedown、wheel两个事件来处理

let eventBus = bpmnViewer.get('eventBus')
let canvas = bpmnViewer.get('canvas')
eventBus.on('element.mousedown', (ev) => {
  if (!this._enables.moveable) {
    ev.preventDefault()
    ev.stopPropagation()
    // event.stopImmediatePropagation()
  }
})

canvas._svg.addEventListener('wheel', (ev) => {
  if (!this._enables.zoomable) {
    // ev.preventDefault()
    ev.stopPropagation()
    // ev.stopImmediatePropagation()
  }
})

国际化/多语言

bpmnjs国际化(汉化)的官方例子:bpmn-js-examples/i18n/

官方的语言包:zn(太少了,很多没翻译过来)

  1. 新建一个translations文件夹,文件夹下新建index.js(翻译器)、zh-cn.js(语言包)
    在这里插入图片描述
  2. index.js代码如下:
import zhCn from './zh-cn'
// https://github.com/bpmn-io/bpmn-js-examples/tree/master/i18n/app/customTranslate
// https://github.com/bpmn-io/bpmn-js-i18n/blob/master/translations/zn.js (不全,很多没翻译过来)

const translations = {
  'zh-cn': zhCn
}

export default function(lang = 'zh-cn') {
  return {
    translate: [
      'value',
      function customTranslate(template, replacements) {
        replacements = replacements || {}

        // 找到目标语言对应的字符串
        template = translations[lang] && translations[lang][template] ? translations[lang][template] : template

        // 替换文本
        return template.replace(/{([^}]+)}/g, function(_, key) {
          return replacements[key] || '{' + key + '}'
        })
      }
    ]
  }
}

  1. zh-cn.js就是对应的语言包,中文语言包是我在官方包基础上改的,并且翻译了官方的属性面板,详见gitee
  2. bpmn.js支持多语言,但不是i18n的那种key(主键)-value(语言字符串)形式的,而是key(模板字符串)-value(目标语言字符串),全量匹配key然后将其替换为value,如下所示:
    在这里插入图片描述
  3. 新建bpmn实例时导入翻译模块即可:
import customTranslateModule from '../translations'

const bpmnModeler = new BpmnModeler({
  additionalModules: [
   customTranslateModule('zh-cn')
  ]
})

判断流程图是否改动

可以通过element.changed事件来检测,图形的新增、删除、属性变化都会触发element.changed

let hasEdited = false
bpmnModeler.on('element.changed',() => { hasEdited = true })

对import生命周期事件的优化

前面已经说到了importXML的一个缺陷,就是在import.done事件后还会触发shape.added或者element.changed事件,这个问题不知道是bpmnjs本来就是这样还是我二次开发做了些什么,没找到什么原因

按照我的常规理解,import.done就是导入已经完成了,如果不对图形操作,不应该再有任何事件抛出

具体解决方法如下:

// #region 触发导入完成事件
let events = ['shape.added', 'element.changed']
let imporFinshedDeb = debounce(() => {
  bpmnModeler.off(events, imporFinshedDeb)
  bpmnModeler.fire('import.finshed')
}, 60)

bpmnModeler.on('import.done', () => {
  console.log('bpmn -> import.done')

  // 先调第一次,防止创建图形时不会触发element.changed
  imporFinshedDeb()

  bpmnModeler.on(events, imporFinshedDeb)
})
// #endregion

获取当前选中元素

推荐监听selection.changed事件来判断当前元素是哪一个,官方的bpmn-js-properties-panel也是这么做的
在这里插入图片描述
在这里插入图片描述

let eventBus = bpmnModeler.get('eventBus'),
  canvas = bpmnModeler.get('canvas'),
  currentElement
eventBus.on('selection.changed', (ev) => {
  let element = ev.newSelection?.[0] ?? canvas.getRootElement()

  if (element.type === 'label') {
    element = element.labelTarget
  }
  currentElement = element 
})

导入与导出的元素id重复

现在有一个场景,需要将一个现有的工作流copy一份出来,重新配置入库:

  1. 使用BpmnModeler实例导出xml并保存到本地
  2. 使用BpmnModeler导入xml再保存到数据库

由于后端插入数据库主键用的是bpmn中元素的id(不是可忽略本小节),此时会产生一个问题,bpmn导入一个xml不会对已有id的元素重新分配ID(可以见源码lib/features/modeling/BpmnFactory.js _ensureId方法),这样会导致copy的工作流中流程、元素的id跟原始工作流的重复了,如果入的是同一个数据库,调用保存接口实际上是修改,而不是新增

具体解决代码如下(我这里是继承了Modeler,super就是Modeler实例):

  importXML(xml, updateID = false) {
    this.clear()

    if (updateID) {
      return super.importXML(xml).then((res) => {
        // let prefix
        let bpmnFactory = this.get('bpmnFactory')

        // 这里是为了解决导出之后再导入,元素ID重复的问题
        this.elementRegistry.forEach((element) => {
          element.businessObject.set('id', '')
          bpmnFactory._ensureId(element.businessObject)
          element.id = element.businessObject.id
          
          // 请勿修改以下代码
          // 见bpmn-js源码:lib/features/modeling/BpmnFactory.js _ensureId方法(44行)
          // if (is(element, 'bpmn:Activity')) {
          //   prefix = 'Activity'
          // } else if (is(element, 'bpmn:Event')) {
          //   prefix = 'Event'
          // } else if (is(element, 'bpmn:Gateway')) {
          //   prefix = 'Gateway'
          // } else if (isAny(element, ['bpmn:SequenceFlow', 'bpmn:MessageFlow'])) {
          //   prefix = 'Flow'
          // } else {
          //   prefix = (element.$type || '').replace(/^[^:]*:/g, '')
          // }

          // prefix += '_'

          // element.id = this.moddle.ids.nextPrefixed(prefix, element)
        })
        return res
      })
    }

    return super.importXML(xml)
  }

其他学习资料

最近在总结本文章的时候,发现了不少优质bpmn相关介绍文章(说实话,之前学习bpmn-js的时候我都没找到,简直痛苦),有些东西我写的不是很详细,可以再看看这几位大佬的总结

后语

  • 写文章不易,如果对你有帮助的话就点个赞吧
  • bpmn-js这个库还是比较庞大的,有些基础原理没有深入了解,比如didi,有错误的话可以在评论中指出
    在这里插入图片描述
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐