在开始技术细节之前,我们先聊聊为什么要在Vue3项目中集成bpmn-js和Activiti。想象一下,你需要开发一个企业级的流程管理系统,比如请假审批、采购流程等。这时候,一个可视化的工作流设计器就变得至关重要。bpmn-js是目前最流行的BPMN 2.0流程建模工具,而Activiti则是Java领域最成熟的工作流引擎之一。
我去年接手过一个OA系统改造项目,客户要求能够在线设计审批流程。当时尝试了几种方案,最终发现Vue3+bpmn-js+Activiti的组合最稳定高效。Vue3的响应式系统让前端开发更简单,bpmn-js提供了专业级的流程设计能力,Activiti则负责后端流程执行。这个组合特别适合需要快速开发工作流系统的团队。
首先,确保你已经安装了最新版的Node.js(建议16.x以上)。然后通过Vue CLI创建一个新项目:
bash复制npm install -g @vue/cli
vue create vue3-bpmn-demo
cd vue3-bpmn-demo
选择Vue3模板后,我们还需要安装Element Plus作为UI框架:
bash复制npm install element-plus
接下来安装bpmn-js及其相关依赖:
bash复制npm install bpmn-js bpmn-js-properties-panel camunda-bpmn-moddle
这里有个小坑要注意:bpmn-js默认使用Camunda的命名空间,而我们需要适配Activiti。虽然现在安装了camunda-bpmn-moddle,但后面会教你如何替换成Activiti的配置。
在src/components目录下新建BpmnDesigner.vue文件,先搭建基础结构:
html复制<template>
<div class="designer-container">
<div class="toolbar">
<el-button-group>
<el-button size="small" @click="saveXML">保存</el-button>
<el-button size="small" @click="undo">撤销</el-button>
</el-button-group>
</div>
<div class="canvas-container">
<div id="bpmn-canvas"></div>
<div id="js-properties-panel" class="properties-panel"></div>
</div>
</div>
</template>
<style scoped>
.designer-container {
height: 100vh;
display: flex;
flex-direction: column;
}
.canvas-container {
flex: 1;
display: flex;
position: relative;
}
#bpmn-canvas {
flex: 1;
border: 1px solid #ccc;
}
.properties-panel {
width: 300px;
border-left: 1px solid #ccc;
}
</style>
在script部分引入所需模块:
javascript复制import { markRaw, onMounted } from 'vue'
import 'bpmn-js/dist/assets/diagram-js.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'
import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css'
import BpmnModeler from 'bpmn-js/lib/Modeler'
import propertiesPanelModule from 'bpmn-js-properties-panel'
import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda'
import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda'
这里有个关键点:bpmnModeler实例必须用markRaw处理。我在第一次集成时就踩了这个坑,设计器能显示但无法拖拽元素。原因是Vue3的响应式代理会干扰bpmn-js的内部逻辑。
javascript复制setup() {
const bpmnModeler = ref(null)
const initBpmn = () => {
const container = document.getElementById('bpmn-canvas')
const modeler = new BpmnModeler({
container,
propertiesPanel: {
parent: '#js-properties-panel'
},
additionalModules: [
propertiesPanelModule,
propertiesProviderModule
],
moddleExtensions: {
camunda: camundaModdleDescriptor
}
})
// 必须使用markRaw
bpmnModeler.value = markRaw(modeler)
modeler.createDiagram().then(() => {
modeler.get('canvas').zoom('fit-viewport')
})
}
onMounted(() => {
initBpmn()
})
return {
bpmnModeler
}
}
bpmn-js默认生成Camunda格式的BPMN XML,而Activiti需要自己的命名空间。我们需要在保存时进行转换:
javascript复制const saveXML = () => {
bpmnModeler.value.saveXML({ format: true }, (err, xml) => {
if (err) {
console.error('保存失败', err)
return
}
// 替换命名空间
const activitiXml = xml
.replace(/camunda.org\/schema\/1.0\/bpmn/g, 'activiti.org/bpmn')
.replace(/camunda:/g, 'activiti:')
console.log('转换后的XML:', activitiXml)
// 这里可以发送到后端保存
})
}
Activiti中常用的用户任务(User Task)在bpmn-js中需要特别设置:
我在项目中封装了一个自动转换的方法,确保所有任务节点都符合Activiti要求:
javascript复制const adaptForActiviti = (xml) => {
// 确保所有任务节点都有正确的类型
let result = xml.replace(/<bpmn:task /g, '<bpmn:userTask ')
// 添加必要的activiti扩展属性
result = result.replace(/<bpmn:userTask([^>]*)>/g, (match, p1) => {
if (!p1.includes('activiti:assignee') && !p1.includes('activiti:candidateGroups')) {
return `<bpmn:userTask${p1} activiti:assignee="${defaultAssignee}">`
}
return match
})
return result
}
利用bpmn-js内置的CommandStack实现撤销重做:
javascript复制const undo = () => {
const commandStack = bpmnModeler.value.get('commandStack')
commandStack.undo()
}
const redo = () => {
const commandStack = bpmnModeler.value.get('commandStack')
commandStack.redo()
}
实现BPMN文件的导入导出:
javascript复制const exportDiagram = () => {
bpmnModeler.value.saveXML({ format: true }, (err, xml) => {
if (err) return
const blob = new Blob([xml], { type: 'application/xml' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'diagram.bpmn'
a.click()
URL.revokeObjectURL(url)
})
}
const importDiagram = (file) => {
const reader = new FileReader()
reader.onload = (e) => {
bpmnModeler.value.importXML(e.target.result, (err) => {
if (err) {
console.error('导入失败', err)
} else {
console.log('导入成功')
}
})
}
reader.readAsText(file)
}
最后,我们实现与Activiti后端的交互。假设后端提供了部署接口:
javascript复制const deployToActiviti = async () => {
bpmnModeler.value.saveXML({ format: true }, async (err, xml) => {
if (err) return
const activitiXml = adaptXmlForActiviti(xml)
try {
const response = await axios.post('/api/deploy', {
processName: 'My Process',
bpmnXml: activitiXml
})
console.log('部署成功:', response.data)
} catch (error) {
console.error('部署失败:', error)
}
})
}
如果遇到设计器显示空白,通常有以下几个原因:
属性面板不显示可能是以下原因:
部署到Activiti失败时,检查:
我在实际项目中总结了一个检查清单,部署前逐一确认:
当流程变得复杂时,可能会遇到性能问题。以下是我总结的优化经验:
例如,按需加载的配置方式:
javascript复制import {
BpmnModeler,
MinimapModule,
MoveCanvasModule,
ZoomScrollModule
} from 'bpmn-js'
const modeler = new BpmnModeler({
container: '#canvas',
modules: [
MinimapModule,
MoveCanvasModule,
ZoomScrollModule
// 只引入必要的模块
]
})
基础功能实现后,可以考虑以下扩展:
例如,添加自定义节点的代码结构:
javascript复制import customModule from './custom-elements'
const modeler = new BpmnModeler({
container: '#canvas',
additionalModules: [
customModule
]
})
在custom-elements.js中定义自定义元素的行为和外观。