1. 流程可视化追踪的核心价值与场景拆解做过审批流、工单流、订单流转这类系统的朋友大概率都碰过同一个需求用户打开页面想一眼看清楚这单货现在走到哪了、谁批过、谁还没批、下一步该找谁。如果只给一张静态流程图用户还得自己拿流程定义去比对体验非常割裂。所以“流程图显示 高亮连线和已执行节点 当前节点”这套组合本质上是把流程引擎里的运行时数据反向映射回流程定义图上让图“活”起来。我先把这套东西的定位说清楚它不是一个独立功能而是流程引擎能力、图形渲染能力、数据映射逻辑三者的交汇点。流程引擎负责告诉你“哪些节点执行过、当前停在哪个节点、哪些连线被走过”图形渲染负责把 BPMN 图正确画出来数据映射负责把引擎里的执行记录精准对应到图上的元素 ID。三者任何一环对不上高亮就会错位、漏亮、或者整条线不亮。适合谁来参考如果你正在做 OA、ERP、CRM、工单系统、审批中台或者任何需要“流程进度可视化”的产品这套思路都能直接套。前端同学可以重点看渲染和映射后端同学可以重点看引擎数据怎么取、怎么组装产品同学可以看交互上到底该亮哪些、不该亮哪些。这里有个容易被忽略的点高亮不是越全越好。我见过一些实现把“已执行节点”和“当前节点”用同一种颜色结果用户根本分不清是走完了还是卡住了。正确的做法是至少三态区分已执行历史、当前进行中、未执行未来。连线同理已走过的连线要亮没走过的保持灰色。这个区分度直接决定用户能不能三秒看懂进度。另外流程图的显示本身也有坑。BPMN 图里节点位置、连线路径都是 XML 里定义的如果渲染时坐标算错高亮元素就会叠在一起或者偏移。所以这套功能的技术难点往往不在“取数据”而在“把数据准确贴到图上”。2. 引擎侧数据获取与状态判定逻辑2.1 已执行节点和连线的数据来源流程引擎在运行过程中会把每一步的执行记录落到历史表里。以常见的 Activiti 系引擎为例历史活动表记录每个活动节点的执行实例历史任务表记录用户任务历史变量表记录流程变量。要判断“哪些节点执行过”最直接的方式就是查历史活动表按流程实例 ID 过滤拿到所有已执行的活动 ID 集合。但这里有个细节历史活动表里同一条记录可能对应多个执行实例比如多实例节点、并行网关分支。如果直接去重后拿 ID 集合可能会漏掉“某个分支走过、另一个分支没走”的情况。所以更稳的做法是按活动 ID 分组同时保留每个活动的执行次数和最后执行时间。这样前端不仅能高亮还能在悬浮时显示“该节点执行过 3 次最后一次是某时刻”。连线的数据来源稍微绕一点。连线在引擎里通常对应“顺序流”它的执行记录不一定单独落表而是通过“当前节点 历史节点”的先后关系推导出来的。具体来说如果节点 A 执行过节点 B 也执行过且 A 到 B 之间存在一条顺序流那这条线大概率被走过。但并行网关场景下A 到 B 和 A 到 C 可能同时走所以推导时要结合网关类型判断。提示不要试图只靠“当前节点”反推连线。当前节点只能告诉你“现在停在哪”不能告诉你“从哪来”。必须结合历史节点集合一起算。2.2 当前节点的精准定位当前节点分两种情况一种是流程停在某个用户任务上等待人工处理另一种是停在某个自动节点上比如服务任务、脚本任务只是瞬间状态。对于可视化来说我们主要关心第一种因为用户需要知道“现在该谁处理”。获取当前节点最直接的方式是查运行时任务表按流程实例 ID 过滤拿到所有待办任务。但要注意并行网关下可能有多个当前节点同时存在。比如一个会签场景三个审批人同时待办那当前节点就有三个。这时候高亮逻辑要支持多节点同时亮而不是只亮一个。还有一种情况是流程已经结束。结束后运行时任务表为空但历史表里有完整记录。这时候“当前节点”应该显示为结束节点或者干脆不显示当前节点只显示已执行路径。我个人的做法是流程结束时把结束事件节点高亮为“已完成”同时所有已执行连线保持高亮给用户一个完整的闭环感。2.3 状态判定的优先级与边界情况状态判定看起来简单实际有一堆边界要处理。我整理了一个优先级顺序供你参考优先级判定条件高亮策略1流程已结束所有已执行节点和连线高亮结束节点特殊标记2存在运行时任务已执行节点高亮当前任务节点用进行中样式已执行连线高亮3存在历史节点但无运行时任务可能是流程被挂起或异常终止按历史节点高亮当前节点标记为异常4无历史无运行时流程未启动全部灰色边界情况里最麻烦的是“驳回”和“跳转”。驳回后原来的已执行节点还算不算已执行我的经验是算。因为用户确实走过那一步只是后来被退回了。但当前节点要更新为驳回后的目标节点。跳转同理跳转前的节点保留已执行状态跳转后的节点作为当前节点。这样用户能看到完整的“走过弯路”的轨迹反而更真实。3. 图形渲染与高亮映射的实操方案3.1 流程图渲染的技术选型渲染 BPMN 图常见方案有三种一是用引擎自带的图生成能力直接输出带高亮的图片二是前端用 SVG 或 Canvas 自己画三是用现成的 BPMN 渲染库比如 bpmn-js 这类。引擎自带图生成的好处是省事后端一个接口返回图片流前端直接展示。但缺点也很明显样式定制困难高亮颜色、线宽、节点图标基本改不动而且每次状态变化都要重新生成图片性能一般。我早期项目用过这种后来因为产品要改高亮样式被迫重构。前端自己画 SVG 的灵活性最高但工作量巨大。BPMN 的连线路径有直线、折线、曲线还有网关的菱形、事件的圆形全部手写坐标计算维护成本很高。除非你的流程图非常简单否则不建议从零造轮子。用现成渲染库是目前最稳的方案。以 bpmn-js 为例它能把 BPMN XML 渲染成 SVG并且暴露了元素注册表你可以通过元素 ID 拿到对应的 SVG 节点然后直接改样式类。这样高亮逻辑就变成“给指定 ID 的元素加一个 CSS 类”非常干净。3.2 元素 ID 映射的关键细节不管用哪种渲染方案核心都是“引擎里的活动 ID”和“图上的元素 ID”要对上。BPMN XML 里每个节点和连线都有 id 属性引擎在部署时会把流程定义解析成活动活动 ID 通常就是 XML 里的 id。所以理论上你从历史表拿到的 activityId直接拿去图上找同名元素就行。但实际项目里有几个坑第一子流程嵌套。如果流程里有子流程子流程内部的活动 ID 可能和外部重复或者引擎在存储时加了前缀。这时候直接按 ID 匹配会错位。解决办法是取历史数据时同时取“执行实例 ID”和“活动 ID”映射时用执行实例的层级关系去定位。第二多实例节点。多实例节点在图上是一个元素但引擎里可能生成多个执行实例。高亮时只需要亮那一个图元素但悬浮提示里要显示“已执行 N 次”。第三连线 ID 的获取。连线在历史表里不一定直接有记录需要从流程定义 XML 里解析出所有顺序流然后根据“源节点已执行 目标节点已执行”来判定。这里建议在部署时就把流程定义的连线关系缓存起来避免每次查询都解析 XML。3.3 高亮样式的设计与实现样式设计上我建议至少定义四套类.highlight-completed已执行节点用浅绿或浅蓝填充边框加深.highlight-current当前节点用橙色或主题色填充加脉冲动画.highlight-completed-line已执行连线用主题色描边线宽加粗.highlight-default未执行元素保持灰色CSS 大概长这样.highlight-completed .djs-visual :first-child { fill: #e6f7e6 !important; stroke: #52c41a !important; } .highlight-current .djs-visual :first-child { fill: #fff7e6 !important; stroke: #fa8c16 !important; stroke-width: 2px !important; } .highlight-completed-line .djs-visual path { stroke: #52c41a !important; stroke-width: 2px !important; }注意!important在这里是必要的因为渲染库自带的样式优先级可能比较高。但不要滥用只在你确认覆盖不掉的时候加。当前节点的脉冲动画可以用 CSS keyframes 实现keyframes pulse { 0% { stroke-width: 2px; } 50% { stroke-width: 4px; } 100% { stroke-width: 2px; } } .highlight-current .djs-visual :first-child { animation: pulse 1.5s infinite; }这样用户一眼就能看到“现在停在这”。4. 完整实操流程与核心代码拆解4.1 后端接口设计与数据组装后端需要提供一个接口入参是流程实例 ID出参是三类数据已执行节点 ID 列表、已执行连线 ID 列表、当前节点 ID 列表。我一般会封装成一个对象{ processInstanceId: proc-123, completedNodeIds: [startEvent1, userTask1, userTask2], completedLineIds: [flow1, flow2], currentNodeIds: [userTask3], finished: false }取数逻辑分三步第一步查历史活动表拿到所有已执行的活动 ID。SQL 大致是SELECT DISTINCT ACT_ID_ FROM ACT_HI_ACTIVITY WHERE PROC_INST_ID_ #{processInstanceId}第二步查运行时任务表拿到当前节点 IDSELECT TASK_DEF_KEY_ FROM ACT_RU_TASK WHERE PROC_INST_ID_ #{processInstanceId}第三步推导已执行连线。这一步需要流程定义的连线关系。我通常会在部署时把 BPMN XML 解析一遍存成“源节点 - 目标节点 - 连线 ID”的映射表。然后遍历已执行节点集合如果某条连线的源和目标都在集合里就把这条连线加入已执行连线列表。注意并行网关下源节点执行了、目标节点也执行了但连线不一定走过。比如 A 到 B 和 A 到 C 是两条分支如果 B 执行了但 C 没执行那 A 到 C 的连线就不该亮。所以判定条件要更严格连线的源节点已执行且目标节点已执行且两者之间存在执行时序关系。时序关系可以从历史活动的开始时间和结束时间推断。4.2 前端渲染与高亮注入前端拿到接口数据后先渲染 BPMN 图然后遍历元素注册表给匹配的 ID 加类。以 bpmn-js 为例const elementRegistry modeler.get(elementRegistry); const canvas modeler.get(canvas); // 清除旧高亮 elementRegistry.forEach(element { canvas.removeMarker(element.id, highlight-completed); canvas.removeMarker(element.id, highlight-current); canvas.removeMarker(element.id, highlight-completed-line); }); // 注入新高亮 data.completedNodeIds.forEach(id { canvas.addMarker(id, highlight-completed); }); data.currentNodeIds.forEach(id { canvas.addMarker(id, highlight-current); }); data.completedLineIds.forEach(id { canvas.addMarker(id, highlight-completed-line); });这里用canvas.addMarker而不是直接操作 DOM是因为渲染库内部会管理元素状态直接改 DOM 可能在重绘时被覆盖。用 marker 的方式重绘后样式依然保留。如果用的是其他渲染方案思路一样找到元素对应的 SVG 节点加类或改属性。关键是不要硬编码坐标一定要通过元素 ID 去查。4.3 参数计算与性能优化当流程节点很多时每次状态变化都全量重绘会卡。我实测下来一个 50 节点的流程全量重绘大概 200ms 左右还能接受。但如果是 200 节点以上就要做增量更新。增量更新的思路是缓存上一次的高亮集合和新集合做差集只移除消失的、只添加新增的。这样大部分情况下只需要操作几个元素性能提升明显。另一个优化点是接口缓存。流程状态在两次查询之间通常不会变可以在前端加一个短时缓存比如 5 秒内重复请求直接返回缓存结果。但要注意用户手动刷新时要强制绕过缓存。还有个细节连线高亮的层级。BPMN 图里连线通常在节点下层如果高亮连线时被节点盖住看起来就像没亮。解决办法是给高亮连线加一个z-index或者调整 SVG 的绘制顺序。bpmn-js 里可以通过canvas.addMarker配合 CSS 的pointer-events和层级处理但更稳的方式是在渲染时就把连线分组放到节点上方。5. 常见问题排查与避坑经验实录5.1 高亮错位与漏亮问题问题一节点亮了但连线没亮。最常见的原因是连线 ID 没取对。很多人以为历史表里会存连线 ID其实不一定。我建议在部署时就把流程定义的连线关系解析出来存成映射运行时直接查映射不要依赖历史表。问题二当前节点亮了但已执行节点没亮。这通常是历史查询条件写错了。比如只查了用户任务没查服务任务和网关。历史活动表里所有类型的活动都有记录查询时不要加类型过滤除非你明确只要用户任务。问题三并行网关下只亮了一个分支。这是因为去重时把不同分支的活动 ID 合并了。解决办法是保留执行实例 ID按实例分组而不是简单去重。问题四驳回后原节点不亮了。有些实现会在驳回时删除历史记录导致原节点丢失。正确做法是保留历史驳回只是新增一条执行记录不删旧的。5.2 样式冲突与渲染异常问题五高亮样式不生效。九成是 CSS 优先级不够。渲染库自带的样式可能用了!important你的类被覆盖了。解决办法是提高选择器权重或者也用!important。但更优雅的方式是通过渲染库的 API 改样式而不是硬覆盖 CSS。问题六脉冲动画导致页面卡顿。如果当前节点很多每个都加动画浏览器重绘压力大。建议只给第一个当前节点加动画或者用will-change提示浏览器优化。问题七流程图缩放后高亮偏移。这通常是渲染库的坐标计算问题不是高亮逻辑问题。检查一下渲染时是否用了正确的 viewBox以及缩放后是否重新计算了元素位置。5.3 常见问题速查表现象可能原因排查方向解决方案节点全灰接口没返回数据查网络请求检查流程实例 ID 是否正确连线不亮连线 ID 映射缺失查映射表部署时解析 XML 存映射当前节点多个并行网关查运行时任务表支持多节点同时高亮驳回后轨迹丢失历史被删查历史表保留历史不物理删除样式不生效CSS 优先级查计算样式提高权重或用 API动画卡顿动画元素过多查性能面板限制动画元素数量5.4 独家避坑技巧第一个技巧在流程部署时就把图元素 ID 和连线关系缓存到本地不要每次查询都解析 XML。解析 XML 是 CPU 密集型操作流程多了会拖慢接口。第二个技巧高亮逻辑和流程引擎解耦。不要把高亮代码写在引擎的监听器里而是做成独立的查询服务。这样引擎升级或换实现时高亮逻辑不用动。第三个技巧给用户一个“刷新”按钮。流程状态可能被其他人操作改变自动轮询有延迟手动刷新能让用户主动获取最新状态。刷新时记得清缓存。第四个技巧测试时覆盖驳回、跳转、并行、子流程、多实例这五种场景。这五种是 bug 高发区普通直线流程反而没问题。第五个技巧高亮颜色要符合无障碍标准。不要只用颜色区分状态最好加上图标或文字标签比如已执行节点加个对勾当前节点加个圆点。色弱用户也能看懂。6. 扩展思路与个人实践体会这套东西做完之后其实还能往上叠不少能力。比如把每个节点的处理人、处理时间、耗时也映射到图上悬浮时显示详情用户就不用跳转到历史列表去查了。再比如把超时节点用红色标记提醒用户赶紧处理。还可以做路径回放按时间顺序依次点亮节点像放动画一样展示流程怎么一步步走过来的。我在实际项目里踩过最大的坑是早期把高亮逻辑写在了前端后端只返回原始历史数据。结果前端要自己推导连线、自己处理并行分支代码越写越乱。后来改成后端统一组装好三类 ID 列表前端只负责加类维护成本直接降了一半。所以我的建议很明确推导逻辑放后端渲染逻辑放前端边界清晰谁也别越界。另外流程图的显示不要追求一次到位。先做静态展示再加已执行高亮再加当前节点最后加连线和动画。每加一层都测试一轮确保不破坏已有功能。我见过有人一口气全做完结果样式冲突、数据错位、性能问题一起爆发排查起来非常痛苦。最后分享一个小技巧如果流程节点特别多可以在图上加一个“聚焦当前节点”的按钮点击后自动滚动到当前节点位置并放大。这个功能用户反馈非常好尤其是长流程场景省去了手动拖拽找节点的麻烦。实现上就是拿到当前节点的 SVG 坐标然后调渲染库的滚动 API。不难但体验提升很明显。