告别后端画图!用bpmn.js在Vue里优雅展示Activiti 6.0高亮流程图(附完整代码)
基于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-js8.7.1 vue2.6.11 element-ui2.15.1 xml-js1.6.112.2 基础组件结构创建流程图展示组件BpmnViewer.vue包含以下核心功能区域工具栏区域缩放控制、视图重置等操作按钮画布区域流程图渲染容器详情面板节点详细信息展示template div classbpmn-container !-- 工具栏 -- div classtoolbar el-button-group el-tooltip content缩小 el-button iconel-icon-zoom-out clickzoomOut/ /el-tooltip el-button{{zoomLevel}}%/el-button el-tooltip content放大 el-button iconel-icon-zoom-in clickzoomIn/ /el-tooltip el-tooltip content重置视图 el-button iconel-icon-refresh clickresetView/ /el-tooltip /el-button-group /div !-- 流程图画布 -- div idbpmn-canvas/div !-- 节点详情 -- div classdetail-panel v-ifcurrentNode h3{{currentNode.name}}/h3 el-descriptions :column1 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 /template3. 核心功能实现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, isMarkerVisibletrue 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 ResponseEntityProcessDiagramDTO getProcessDiagram( PathVariable String instanceId) { // 获取流程定义 ProcessInstance instance runtimeService.createProcessInstanceQuery() .processInstanceId(instanceId) .singleResult(); String definitionId instance.getProcessDefinitionId(); ProcessDefinition definition repositoryService.getProcessDefinition(definitionId); // 获取历史活动记录 ListHistoricActivityInstance 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, ListHistoricActivityInstance 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/svgxml;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, $ isMarkerVisibletrue ); }问题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] ); } }); // 实现手势缩放逻辑 // ...