前言
本文以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觉得功能比较完善,因为几乎没有文档被我忽略了,在一次交流中后端同事说准备采用activiti和camunda这两个开源流程引擎做参考来实现自研工作流引擎(这两个引擎的前端都是基于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')
具体类型定义见此
属性面板
流程图的绘制基本实现了,但一个完整的工作流还需要在其流程、每个节点、流转上配置一些属性,比如审核规则、参与者、流程名称、变量等,属性面板的作用就是如此。
官方属性面板
如果你想使用官方提供的属性面板,需要先安装两个插件:
- bpmn-js-properties-panel@0.46.0(注意版本,0.46.0后面就是1.0.0版本,改动比较大,详见更新日志)
- camunda-bpmn-moddle@6.1.2
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,学习后发现有以下缺点:
- 没有文档,只有一些例子,并且自定义属性的例子看不懂
- 不知道如何自定义下拉框组件的数据来源,也不知道如何自定义输入器
- 配置的属性会放到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(太少了,很多没翻译过来)
- 新建一个translations文件夹,文件夹下新建index.js(翻译器)、zh-cn.js(语言包)
- 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 + '}'
})
}
]
}
}
- zh-cn.js就是对应的语言包,中文语言包是我在官方包基础上改的,并且翻译了官方的属性面板,详见gitee
- bpmn.js支持多语言,但不是i18n的那种key(主键)-value(语言字符串)形式的,而是key(模板字符串)-value(目标语言字符串),全量匹配key然后将其替换为value,如下所示:
- 新建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一份出来,重新配置入库:
- 使用BpmnModeler实例导出xml并保存到本地
- 使用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,有错误的话可以在评论中指出