基于bpmn.js的Activiti 6.0流程可视化方案设计与实现
在当今企业级应用开发中,流程引擎扮演着越来越重要的角色。Activiti作为一款成熟的开源工作流引擎,其6.0版本带来了诸多改进,但也移除了原有的流程图跟踪组件Diagram Viewer,这给开发者带来了新的挑战。本文将详细介绍如何利用bpmn.js在Vue框架中实现Activiti 6.0流程图的动态渲染与高亮展示,为开发者提供一套完整的解决方案。
1. 技术选型与架构设计
传统Activiti流程展示方案通常采用后端生成静态图片的方式,这种方式存在几个明显缺陷:
- 字体兼容性问题:不同操作系统环境下字体渲染不一致
- 性能瓶颈:高并发场景下图片生成压力大
- 交互性差:无法实现动态交互效果
- 维护成本高:每次流程变更都需要重新生成图片
相比之下,基于bpmn.js的前端渲染方案具有以下优势:
| 对比维度 | 后端图片方案 | bpmn.js前端方案 |
|---|---|---|
| 渲染方式 | 静态图片 | 动态SVG |
| 交互能力 | 无 | 支持缩放、高亮、悬浮提示等 |
| 性能表现 | 服务器压力大 | 客户端渲染,减轻服务器负担 |
| 兼容性 | 字体依赖严重 | 纯前端实现,跨平台一致 |
| 开发效率 | 修改需重新部署 | 热更新,即时生效 |
技术栈组成:
// package.json核心依赖 { "dependencies": { "bpmn-js": "^8.7.1", // 核心流程图渲染库 "vue": "^2.6.11", // 前端框架 "element-ui": "^2.15.1", // UI组件库 "xml-js": "^1.6.11" // XML处理工具 } }2. 环境搭建与基础配置
2.1 项目初始化
首先创建一个标准的Vue项目,并安装必要依赖:
# 创建Vue项目 vue create bpmn-viewer-demo # 进入项目目录 cd bpmn-viewer-demo # 安装核心依赖 npm install bpmn-js@8.7.1 vue@2.6.11 element-ui@2.15.1 xml-js@1.6.112.2 基础组件结构
创建流程图展示组件BpmnViewer.vue,包含以下核心功能区域:
- 工具栏区域:缩放控制、视图重置等操作按钮
- 画布区域:流程图渲染容器
- 详情面板:节点详细信息展示
<template> <div class="bpmn-container"> <!-- 工具栏 --> <div class="toolbar"> <el-button-group> <el-tooltip content="缩小"> <el-button icon="el-icon-zoom-out" @click="zoomOut"/> </el-tooltip> <el-button>{{zoomLevel}}%</el-button> <el-tooltip content="放大"> <el-button icon="el-icon-zoom-in" @click="zoomIn"/> </el-tooltip> <el-tooltip content="重置视图"> <el-button icon="el-icon-refresh" @click="resetView"/> </el-tooltip> </el-button-group> </div> <!-- 流程图画布 --> <div id="bpmn-canvas"></div> <!-- 节点详情 --> <div class="detail-panel" v-if="currentNode"> <h3>{{currentNode.name}}</h3> <el-descriptions :column="1" border> <el-descriptions-item label="审批人">{{currentNode.assignee}}</el-descriptions-item> <el-descriptions-item label="状态">{{currentNode.status}}</el-descriptions-item> <el-descriptions-item label="审批意见">{{currentNode.comment}}</el-descriptions-item> </el-descriptions> </div> </div> </template>3. 核心功能实现
3.1 流程图渲染与高亮
流程图渲染的核心流程包括:
- 初始化bpmn.js Viewer实例
- 从后端获取流程XML定义
- 解析并渲染流程图
- 根据执行状态添加高亮标记
import BpmnViewer from 'bpmn-js'; export default { data() { return { bpmnViewer: null, zoomLevel: 100, currentNode: null }; }, mounted() { this.initViewer(); this.loadProcessDiagram(); }, methods: { initViewer() { this.bpmnViewer = new BpmnViewer({ container: '#bpmn-canvas', height: '600px', width: '100%' }); }, async loadProcessDiagram() { try { // 从后端获取流程定义XML const { xml, highlights } = await this.fetchProcessData(); // 处理特殊节点(如排他网关) const processedXml = this.preprocessXml(xml); // 渲染流程图 await this.bpmnViewer.importXML(processedXml); // 添加高亮标记 this.applyHighlights(highlights); // 自适应视图 this.bpmnViewer.get('canvas').zoom('fit-viewport'); } catch (error) { console.error('流程图加载失败:', error); } }, preprocessXml(xml) { // 处理排他网关显示标记 return xml.replace( /<bpmndi:BPMNShape[^>]*bpmnElement="([^"]*)"[^>]*>/g, (match, id) => { if (id.includes('Gateway')) { return match.replace('bpmnElement="', 'isMarkerVisible="true" bpmnElement="'); } return match; } ); }, applyHighlights(highlights) { const canvas = this.bpmnViewer.get('canvas'); // 已完成节点高亮 highlights.executedNodes.forEach(nodeId => { canvas.addMarker(nodeId, 'highlight-executed'); }); // 当前活动节点高亮 highlights.activeNodes.forEach(nodeId => { canvas.addMarker(nodeId, 'highlight-active'); }); // 已流转连线高亮 highlights.completedFlows.forEach(flowId => { canvas.addMarker(flowId, 'highlight-flow'); }); } } };3.2 交互功能实现
为提升用户体验,我们需要实现以下交互功能:
- 缩放控制:支持流程图的放大、缩小和重置
- 节点悬停:显示节点详细信息
- 视图导航:快速定位到特定节点
methods: { // 缩放控制 zoomIn() { this.zoomLevel = Math.min(this.zoomLevel + 10, 400); this.bpmnViewer.get('canvas').zoom(this.zoomLevel / 100); }, zoomOut() { this.zoomLevel = Math.max(this.zoomLevel - 10, 20); this.bpmnViewer.get('canvas').zoom(this.zoomLevel / 100); }, resetView() { this.zoomLevel = 100; this.bpmnViewer.get('canvas').zoom('fit-viewport'); }, // 节点交互 setupNodeInteractions() { const eventBus = this.bpmnViewer.get('eventBus'); // 节点悬停事件 eventBus.on('element.hover', event => { if (event.element.type === 'bpmn:UserTask') { this.fetchNodeDetails(event.element.id); } }); eventBus.on('element.out', () => { this.currentNode = null; }); }, async fetchNodeDetails(nodeId) { try { const response = await axios.get(`/api/process/nodes/${nodeId}`); this.currentNode = response.data; } catch (error) { console.error('获取节点详情失败:', error); } } }4. 样式优化与视觉效果
良好的视觉效果可以显著提升用户体验。我们通过CSS实现以下效果:
- 节点高亮样式:区分已完成、进行中和待处理节点
- 连线高亮:突出显示已流转的路径
- 动画效果:为当前节点添加呼吸灯效果
/* 基础样式 */ #bpmn-canvas { border: 1px solid #eee; background: white; margin-top: 10px; } /* 高亮样式 */ .highlight-active .djs-visual > :nth-child(1) { fill: #FFF3E0 !important; stroke: #FFA726 !important; stroke-width: 2px; animation: pulse 2s infinite; } .highlight-executed .djs-visual > :nth-child(1) { fill: #E8F5E9 !important; stroke: #66BB6A !important; } .highlight-flow .djs-visual > :nth-child(1) { stroke: #66BB6A !important; stroke-width: 2px; } /* 呼吸灯动画 */ @keyframes pulse { 0% { stroke-width: 2px; } 50% { stroke-width: 4px; } 100% { stroke-width: 2px; } } /* 详情面板样式 */ .detail-panel { margin-top: 20px; padding: 15px; border: 1px solid #ebeef5; border-radius: 4px; background: #fafafa; }5. 后端接口设计与数据格式
前端需要后端提供以下关键数据:
- 流程定义的XML字符串
- 已执行节点ID列表
- 当前活动节点ID列表
- 已流转的连线ID列表
推荐返回数据结构:
{ "modelXml": "<bpmn2:definitions>...</bpmn2:definitions>", "highlights": { "executedNodes": ["task1", "task2"], "activeNodes": ["task3"], "completedFlows": ["flow1", "flow2"] }, "processInfo": { "name": "请假审批流程", "version": 1 } }Java后端示例代码:
@RestController @RequestMapping("/api/process") public class ProcessController { @Autowired private RepositoryService repositoryService; @Autowired private HistoryService historyService; @GetMapping("/diagram/{instanceId}") public ResponseEntity<ProcessDiagramDTO> getProcessDiagram( @PathVariable String instanceId) { // 获取流程定义 ProcessInstance instance = runtimeService.createProcessInstanceQuery() .processInstanceId(instanceId) .singleResult(); String definitionId = instance.getProcessDefinitionId(); ProcessDefinition definition = repositoryService.getProcessDefinition(definitionId); // 获取历史活动记录 List<HistoricActivityInstance> activities = historyService .createHistoricActivityInstanceQuery() .processInstanceId(instanceId) .orderByHistoricActivityInstanceStartTime().asc() .list(); // 构建高亮数据 ProcessHighlights highlights = buildHighlights(definitionId, activities); // 获取BPMN XML BpmnModel model = repositoryService.getBpmnModel(definitionId); String xml = convertModelToXml(model); // 返回结果 ProcessDiagramDTO result = new ProcessDiagramDTO(); result.setModelXml(xml); result.setHighlights(highlights); result.setProcessName(definition.getName()); return ResponseEntity.ok(result); } private ProcessHighlights buildHighlights(String definitionId, List<HistoricActivityInstance> activities) { // 实现高亮数据构建逻辑 // ... } private String convertModelToXml(BpmnModel model) { // 实现BPMN模型转XML逻辑 // ... } }6. 性能优化与异常处理
在实际生产环境中,我们需要考虑以下优化点:
- XML缓存:避免每次请求都重新生成流程定义XML
- 懒加载:对于大型流程图,可采用分片加载策略
- 错误边界:妥善处理各种异常情况
优化后的前端代码:
{ methods: { async loadProcessDiagram() { this.loading = true; try { // 检查本地缓存 const cacheKey = `process-${this.instanceId}`; const cached = localStorage.getItem(cacheKey); if (cached) { const { xml, highlights, timestamp } = JSON.parse(cached); // 检查缓存是否过期(1小时) if (Date.now() - timestamp < 3600000) { await this.renderDiagram(xml, highlights); return; } } // 无缓存或已过期,从服务器获取 const response = await axios.get(`/api/process/${this.instanceId}`); const { modelXml, highlights } = response.data; // 更新缓存 localStorage.setItem(cacheKey, JSON.stringify({ xml: modelXml, highlights, timestamp: Date.now() })); await this.renderDiagram(modelXml, highlights); } catch (error) { this.$notify.error({ title: '加载失败', message: '流程图加载失败,请稍后重试' }); console.error('流程图加载错误:', error); } finally { this.loading = false; } }, async renderDiagram(xml, highlights) { try { // 尝试渲染 await this.bpmnViewer.importXML(xml); this.applyHighlights(highlights); this.setupNodeInteractions(); // 初始缩放 this.bpmnViewer.get('canvas').zoom('fit-viewport'); } catch (error) { // 处理渲染错误 if (error.warnings && error.warnings.length) { console.warn('流程图渲染警告:', error.warnings); } else { throw error; } } } } }7. 扩展功能与进阶技巧
在基础功能之上,我们可以进一步扩展以下高级功能:
- 流程模拟:集成bpmn-js-token-simulation实现流程模拟
- 属性面板:展示和编辑元素属性
- 自定义元素:扩展支持公司特有的流程节点类型
流程模拟集成示例:
import TokenSimulation from 'bpmn-js-token-simulation'; // 初始化Viewer时添加模拟模块 this.bpmnViewer = new BpmnViewer({ container: '#bpmn-canvas', additionalModules: [ TokenSimulation ] }); // 添加模拟控制按钮 methods: { startSimulation() { const simulation = this.bpmnViewer.get('tokenSimulation'); simulation.start(); }, stopSimulation() { const simulation = this.bpmnViewer.get('tokenSimulation'); simulation.stop(); } }自定义元素样式:
// 注册自定义渲染器 this.bpmnViewer.get('modeling').updateProperties(element, { 'custom:customColor': '#FF5722', 'custom:customIcon': 'data:image/svg+xml;utf8,...' }); // 自定义渲染逻辑 const customRenderer = { canRender: (element) => { return element.businessObject.get('custom:isCustom'); }, drawShape: (parentNode, element) => { // 实现自定义绘制逻辑 // ... } }; this.bpmnViewer.get('elementRegistry').addRenderer(customRenderer);8. 最佳实践与常见问题
在实际项目中,我们总结了以下最佳实践:
性能优化:
- 对于大型流程图,考虑分阶段渲染
- 使用Web Worker处理XML解析
- 实现虚拟滚动只渲染可视区域内的元素
常见问题解决:
问题1:排他网关标记不显示
解决方案:在XML中为对应的BPMNShape添加isMarkerVisible属性
function fixGatewayMarkers(xml) { return xml.replace( /<bpmndi:BPMNShape[^>]*bpmnElement="([^"]*Gateway[^"]*)"[^>]*>/g, '$& isMarkerVisible="true"' ); }问题2:自定义样式不生效
解决方案:确保CSS选择器优先级足够高,必要时添加!important
/* 提高选择器特异性 */ div#bpmn-canvas .highlight .djs-visual > :nth-child(1) { fill: red !important; }问题3:流程图渲染错位
解决方案:确保在容器可见后再初始化Viewer,或在容器尺寸变化时调用canvas.resized()
this.$nextTick(() => { this.initViewer(); this.loadProcessDiagram(); }); // 处理窗口大小变化 window.addEventListener('resize', () => { if (this.bpmnViewer) { this.bpmnViewer.get('canvas').resized(); } });- 移动端适配:
- 添加手势支持(双指缩放、拖动)
- 优化触摸反馈
- 调整UI元素大小确保可操作性
// 触摸事件处理 document.getElementById('bpmn-canvas').addEventListener('touchstart', (e) => { if (e.touches.length === 2) { this.startPinchDistance = getDistance( e.touches[0], e.touches[1] ); } }); // 实现手势缩放逻辑 // ...