简介:面向Vue开发者的流程图编辑器组件,基于Vue与ES6实现,可用于搭建节点拖拽、连线编辑、快捷创建链接等交互式流程编辑功能,适合在后台管理系统、低代码平台中嵌入可视化编排能力。压缩包共23个文件、约118KB,以JavaScript脚本、Vue单文件组件、HTML示例页面为主,另有完整说明文档、许可证及工程配置,结构精简、便于拆解。组件支持拖拽、快捷连线、连线编辑、对齐参考线等典型开关配置;源码对节点、连线、事件、菜单、标线等模块做了清晰拆分,读者既可安装依赖后直接使用,也可对照示例与构建配置,深入学习Vue组件封装方式和流程图交互设计。目前已有16496人学习下载,适合中高级前端开发者用于二次开发或自定义流程编辑组件,在项目集成和代码扩展方面都比较友好。
1. 流程图组件选型:vue-super-flow 把流程建模变成了改 JSON
做流程可视化绕不开流程图组件,但真到选型时你会发现:要么重、要么贵、要么和 Vue 的数据流别着劲。vue-super-flow 走的是另一条路——专注于数据驱动画布,把节点、连线、端口建模成普通 JSON,业务层改数据,画布自己响应。对审批流、任务编排、拓扑展示这类场景,它能把开发成本从几周压到几天。本文按一套完整落地流程拆解它:从数据模型到接入配置,从交互定制到线上排查,最后落到与业务系统对接的序列化与性能技巧。适合正在做流程可视化、又不愿被复杂图形库绑架的前端开发者。
2. 节点—连线—端口:先把流程图拆成一张数据表
2.1 数据驱动:节点与连线在 JS 里到底是什么
流程图组件和图表组件最大的区别在于:它不只是一张静态画布,而是一份可编辑、可持久化的结构化数据。vue-super-flow 的数据模型和大多数流程图组件一致,由三部分组成——节点表、连线表,端口则挂在节点上作为连线的锚点。业务侧维护的是纯对象数组,组件内部才去操作 SVG 或 Canvas。
节点最小结构,我一般会这样定义:
// 节点最小数据模型 const node = { id: 'node_start_001', // 全局唯一,连线和事件都靠它定位 type: 'start', // 节点类型,决定渲染成什么形状 x: 120, // 画布坐标系下的 x 坐标 y: 80, // 画布坐标系下的 y 坐标 ports: [ // 端口列表,连线从这里引出或接入 { id: 'port_out', position: 'bottom', type: 'output' }, { id: 'port_in', position: 'top', type: 'input' } ], data: { label: '开始节点' } // 业务自定义字段,渲染时取用 }逻辑说明:id 是组件内部的定位锚点,连线记录的是「源节点端口到目标节点端口」,所以节点 id 和端口 id 都必须保持稳定,不要用数组下标代替。x、y 存的是画布坐标而不是屏幕坐标,一旦画布发生缩放或平移,组件内部会做坐标换算,业务侧不需要关心换算过程。data 字段是自定义数据的出口,自定义节点里的标签、状态、颜色都可以塞进去。
参数说明:type 字段对应组件注册的节点类型,未注册的类型会走默认渲染,我建议把 type 收敛成枚举,避免字符串散落各处。position 是端口的相对方位,最终展示位置由组件根据节点宽高计算,不用手填像素值;output 表示连线起点,input 表示连线终点,两者混用是方向错乱的根源。
连线的最小结构更简单:
// 连线最小数据模型 const edge = { id: 'edge_001', sourceNodeId: 'node_start_001', // 源节点 id sourcePortId: 'port_out', // 源端口 id targetNodeId: 'node_approve_001', // 目标节点 id targetPortId: 'port_in', // 目标端口 id label: '提交', // 连线上显示的文字 type: 'bezier' // 曲线类型,常见还有 straight }关键点在于 source 和 target 是成对出现的,改节点时如果只更新节点 id 而忘了同步连线,画布上就会出现断线。我一般会写一个 rebuild 函数,在节点删除或变更后重新映射连线,而不是手动去改每条 edge。给这些字段传入非法值时,组件会在控制台抛出连线校验警告,但不会阻止渲染,这是设计上的宽容,线上排查时反而要主动检查。
2.2 端口方向与连线逻辑:为什么画出来箭头是反的
端口方向是初学者最容易翻车的地方。现象是:从 A 节点拖到 B 节点,箭头却从 B 指向 A。原因在于组件默认把源端口的 output 和目标端口的 input 绑定,如果两个节点都把端口定义成 output,组件不知道该以谁为起点,就会按数据写入顺序或坐标位置取默认。
解决方法是端口类型和连线方向强校验:起点必须是 output 型端口,终点必须是 input 型端口,类型不匹配时连接被拒绝。端口类型按这个表来设计:
| 端口类型 | 方向含义 | 典型位置 | 适用场景 |
|---|---|---|---|
| output | 连线起点,只能作为 source | bottom / right | 流程的开始节点、普通节点的出口 |
| input | 连线终点,只能作为 target | top / left | 审批节点、汇聚节点的入口 |
| both | 双向可用 | 左右各一 | 中间处理节点、既有入又有出的节点 |
我通常在两个地方做校验:一是数据结构层面,写一个类型守卫函数,遍历所有 edge 检查 sourcePort 和 targetPort 的类型,不匹配直接抛错;二是交互层面,组件提供了 before-edge-connect 事件,允许业务层在连线建立前拦截,返回 false 就回弹,这个事件后面细说。端口位置还有一个细节:节点尺寸变化后,端口跟着变化的计算时机如果不对,就会出现连线错位,这一点在第 5 章会展开。
2.3 最小可运行数据:三节点两连线跑通一次渲染
把上面的结构拼成一份最小数据集,三个节点、两条边,覆盖「开始、审批、结束」的闭环:
{ "nodes": [ { "id": "n1", "type": "start", "x": 80, "y": 120, "ports": [ { "id": "n1_out", "position": "right", "type": "output" } ], "data": { "label": "创建工单" } }, { "id": "n2", "type": "approve", "x": 320, "y": 120, "ports": [ { "id": "n2_in", "position": "left", "type": "input" }, { "id": "n2_out", "position": "right", "type": "output" } ], "data": { "label": "组长审批" } }, { "id": "n3", "type": "end", "x": 560, "y": 120, "ports": [ { "id": "n3_in", "position": "left", "type": "input" } ], "data": { "label": "归档" } } ], "edges": [ { "id": "e1", "sourceNodeId": "n1", "sourcePortId": "n1_out", "targetNodeId": "n2", "targetPortId": "n2_in", "type": "bezier" }, { "id": "e2", "sourceNodeId": "n2", "sourcePortId": "n2_out", "targetNodeId": "n3", "targetPortId": "n3_in", "type": "bezier" } ] }这份 JSON 直接传给画布组件就能渲染出一张可拖动的流程图。跑通后再逐步增加字段,比如给节点加背景色、给连线加条件文案。从最小集开始的好处是:出问题时能快速定位是数据结构不对,还是组件配置不对,而不是在一堆业务字段里翻找。
提示:节点 id 和端口 id 尽量不要用自增数字,因为业务上经常要插入或删除节点,数字一旦复用,连线会接到错误的节点上。用带语义的前缀最省心。
3. 快速接入:安装组件、注册画布、渲染第一张业务图
3.1 安装与注册:全局注册还是局部注册
安装依赖直接走 npm:
npm install vue-super-flow安装完成后,全局注册的写法比较省事,适合项目里多处都要用流程图的场景:
import Vue from 'vue' import SuperFlow from 'vue-super-flow' import 'vue-super-flow/dist/style.css' Vue.use(SuperFlow)如果流程图只在某个详情页或配置页出现一次,我更推荐局部注册,减少首屏不必要的组件初始化:
import { SuperFlowCanvas } from 'vue-super-flow' import 'vue-super-flow/dist/style.css' export default { components: { SuperFlowCanvas } }逻辑说明:全局注册会一次性把画布、小地图、节点面板等子组件全部挂到 Vue 原型链上,适合后台管理系统中多个路由页都要画流程图的场景;局部注册则依赖 ES6 的 import 按需解析,构建时只会打进用到的那部分代码。CSS 必须引入,否则画布坐标系、端口偏移和连线样式全部错乱,这是大多数集成翻车的第一现场。
3.2 渲染第一张图:模板、数据、样式三步走
模板部分留一个画布容器,传入节点和连线数据,并监听常用事件:
<template> <div class="flow-container"> <super-flow-canvas :nodes="nodes" :edges="edges" :zoom-range="[0.5, 2]" background @node-click="handleNodeClick" @node-drag-end="handleNodeDragEnd" @edge-connect="handleEdgeConnect" /> </div> </template>script 部分维护数据:
export default { data() { return { nodes: [], // 初始为空,mounted 中加载 edges: [] } }, mounted() { // 模拟异步加载流程图数据 this.loadFlowData() }, methods: { loadFlowData() { // 实际项目里这里是 axios.get('/api/flow/detail') const res = this.buildMockData() this.nodes = res.nodes this.edges = res.edges }, handleNodeClick(node) { console.log('点击了节点', node.id, node.data) }, handleNodeDragEnd({ node, position }) { const target = this.nodes.find(item => item.id === node.id) if (target) { target.x = position.x target.y = position.y } }, handleEdgeConnect({ sourceNodeId, targetNodeId }) { // 交给后端校验或直接写入队列 } } }逻辑说明:画布的高度由父容器决定,所以 .flow-container 只设置 width 和 height,不要让它高度塌缩。初始化时 nodes 和 edges 置空,数据加载完成后再赋值,组件内部会做一次全量重绘。事件回调里收到的坐标是画布坐标系下的数值,直接写回节点对象即可,不需要换算。
参数说明:zoom-range 控制缩放比例上下限,最小 0.5 最大 2 是比较常用的业务区间;background 启用网格背景,没有它拖节点时缺少位置参照,视觉上很难受。@edge-connect 在连线成功建立时触发,适合做业务合法性校验,比如禁止跳过审批节点直连结束节点。
3.3 配置项:缩放、拖动与连线样式对照表
接入初期最常调的配置项,我汇总成一张表,按「先画布、后连线、再交互」的顺序逐个调:
| 配置项 | 默认值 | 作用 | 常见坑 |
|---|---|---|---|
| zoom-range | [0.25, 2.5] | 画布缩放范围 | 设得过大,小屏设备上节点会糊 |
| zoom-on-scroll | true | 滚轮缩放开关 | 与页面滚动冲突时改成 false |
| draggable | true | 是否允许拖动画布 | 节点拖拽事件会被画布平移抢占 |
| edge-type | bezier | 默认连线类型 | 直线更易读,但交叉多时用 bezier |
| edge-style | {} | 连线样式覆盖 | 线宽和颜色要区分选中/默认态 |
| default-node-size | [160, 60] | 节点默认宽高 | 与自定义节点实际尺寸不一致会错位 |
| show-minimap | false | 小地图开关 | 节点多了才建议开,有性能开销 |
从这里能看到组件的设计取向:配置项控制的是画布行为,而不是具体业务样式,节点长什么样交给自定义节点去实现。draggable 和节点拖拽的冲突是排查高频问题,第 5 章会单独讲。
4. 交互与定制:拖拽建连、自定义节点与事件回写
4.1 拖拽连线与坐标变化:让图上操作落回数据
流程图组件和静态展示图最大的差异在交互:用户拖拽节点、手动连线、缩放平移,这些动作最终都要回写到数据模型。vue-super-flow 的事件体系覆盖了这几类操作,我在接入时只用了其中最核心的三个:node-drag-end、edge-connect、edge-disconnect。
// 手动连线完成后的回调 handleEdgeConnect(payload) { const { sourceNodeId, sourcePortId, targetNodeId, targetPortId } = payload // 业务校验:不允许开始节点同时是两条连线的终点 const duplicated = this.edges.some( edge => edge.targetNodeId === sourceNodeId ) if (duplicated) { // 拦截连线,组件会回弹 return false } // 校验通过,把新连线写进数据 this.edges.push({ id: 'edge_' + Date.now(), sourceNodeId, sourcePortId, targetNodeId, targetPortId }) return true }逻辑说明:payload 里带的是节点 id 和端口 id 的完整组合,业务层拿到后先做校验,返回 false 会让连线消失,返回 true 则保留。这里有个容易漏的点:组件只负责渲染和交互,它不会把新连线自动写进你的数据源。如果不手动 push,刷新页面后连线就丢了。
参数说明:sourcePortId 是端口 id 而不是端口类型,校验时如果需要判断类型,要通过节点数据里对应端口对象的 type 字段取。拖拽节点的回调处理类似,node-drag-end 返回拖拽结束后的最终坐标,高频更新放在 drag 过程中会有性能问题,所以只监听结束事件。
4.2 自定义节点:用插槽还是渲染函数
默认节点只适合原型阶段,业务系统里的节点通常要带图标、状态色、操作按钮。vue-super-flow 提供两种扩展方式:插槽覆盖和渲染函数注册。
插槽方式适合改样式,比如给审批节点加一个状态角标:
<super-flow-canvas :nodes="nodes" :edges="edges"> <template #node-approve="{ node }"> <div class="approve-node"> <span class="approve-icon">审</span> <span class="approve-label">{{ node.data.label }}</span> <span class="approve-status" :class="'status-' + node.data.status" >{{ node.data.status }}</span> </div> </template> </super-flow-canvas>逻辑说明:插槽名遵循 node-类型 的命名规则,组件渲染时按节点 type 匹配对应插槽,匹配不到走默认渲染。作用域插槽里能拿到当前节点完整对象,包括 data 字段,所以业务属性都往 data 里塞就好。
参数说明:插槽方案适合一个项目里只有少数几种节点类型的情况,每种类型对应一套模板。如果节点类型多且逻辑复杂,需要根据节点数据动态生成不同结构,那就改用渲染函数:
import { registerNode } from 'vue-super-flow' registerNode('parallel-gateway', (h, node) => { const statusColor = { active: '#1890ff', done: '#52c41a', error: '#ff4d4f' } return h('div', { class: 'gateway-node', style: { borderColor: statusColor[node.data.status] } }, [ h('span', node.data.label), h('em', node.data.branchCount + ' 条分支') ]) })逻辑说明:渲染函数比插槽更贴近组件内部实现,适合节点结构随着字段变化的场景。h 是创建虚拟节点的函数,节点样式直接写在返回的 vnode 上,可以读取 node.data 里任意字段做条件渲染。同一个 type 的节点在不同业务页面里想呈现不同结构,优先用它,避免写一堆 v-if 的插槽模板。
4.3 事件与数据同步:图只是视图,数据才是真相
用了一段时间后我发现,流程图组件在项目里的正确位置是「受控组件」:数据是唯一真相来源,图上的每一次交互都必须回写到数据,再由数据驱动重新渲染。如果绕过数据直接操作 DOM,或者依赖组件内部缓存的状态,早晚会出现图上和图下不一致的诡异问题。
我一般会固定一套数据流:组件派发事件,业务层改数据,数据通过 props 回流到组件。这样既方便在数据层做校验和埋点,也让流程图与表单、明细表等其他视图共用同一份数据时保持一致。配合 Vue 的 watch,还能在节点数据变化时自动更新关联的连线和侧边栏。
5. 避坑排查:流程图组件最常见的五个翻车点
5.1 节点拖不动,画布却跟着平移
现象:鼠标按住节点拖动,节点纹丝不动,整个画布背景反而在移动。
原因:节点元素的拖拽事件被画布平移抢占。画布平移通常绑定在 mousedown 上,而节点的 mousedown 没有阻止冒泡,事件一路冒泡到画布层,画布以为你要移动整块画布。
解决:在节点根元素绑定事件时加 .stop 修饰符,或者手动调用 e.stopPropagation()。我习惯在自定义节点模板最外层统一加上 @mousedown.stop,一行代码解决,同时也避免了后续加按钮时出现同类问题。
5.2 连线错位,箭头悬空
现象:画布缩放或节点宽度变化后,连线起始点和端口明显对不上,箭头像是悬浮在半空。
原因:连线端点的计算依赖端口的绝对坐标,端口坐标在节点尺寸变化后需要触发重算。如果数据更新后没有通知组件刷新,或者组件在动画帧内没有同步几何信息,就会出现漂移。
解决:更新节点数据后,等待 DOM 渲染完成再让组件重新计算端口位置,通常是在赋值后调用 nextTick 里触发一次重绘。我排查时会先确认节点尺寸是组件计算的还是自定义样式撑开的,自定义节点里给根元素写死宽高,能减少九成坐标不同步的玄学问题。
5.3 删除节点后留下幽灵连线
现象:删掉流程中的一个节点,画布上还残留着连向该节点的虚线,点击也没有任何反应。
原因:只删了节点数据,没删关联的 edges。组件渲染时发现 edge 引用了不存在的节点 id,降级成了不可交互的幽灵线。
解决:删除时做级联处理,把所有 source 或 target 指向该节点的 edge 一并过滤掉:
removeNode(nodeId) { this.nodes = this.nodes.filter(node => node.id !== nodeId) this.edges = this.edges.filter( edge => edge.sourceNodeId !== nodeId && edge.targetNodeId !== nodeId ) }逻辑说明:过滤操作要保证不可变,用 filter 返回新数组,而不是 splice 原地改,否则 Vue 的响应式系统检测不到变化。这套逻辑同样适用于批量删除和复制粘贴场景。
5.4 缩放后点击位置对不上
现象:页面缩放后点击节点,高亮圈出现在旁边空白处,位置偏移量随缩放比例变化。
原因:事件回调里拿到的坐标没有做坐标系换算。端口和节点的坐标存的是画布坐标,而鼠标事件给的是屏幕坐标,两者之间隔着 zoom 缩放系数和画布平移偏移量。
解决:组件提供了一些坐标转换 API 或事件参数里直接带转换好的坐标,优先使用事件参数。如果你是在自定义节点里监听鼠标事件,需要手动换算:画布坐标等于屏幕坐标减画布偏移量再除以缩放系数。不要在自定义节点里用 getBoundingClientRect 去算,节点本身可能带着 transform 变换,算出来永远是错的。
5.5 数据更新了,图却不刷新
现象:修改 nodes 数组某个节点的 data 字段,画布上的文字和样式没有任何变化,控制台也不报错。
原因:直接修改了数组里某个对象的新增属性,这个属性不是响应式的。Vue 2 的响应式系统对对象新增属性是侦测不到的。
解决:给对象补属性要用 Vue.set,或者干脆在初始化时就定义好所有字段;替换整个对象或数组也能触发更新。我一般把节点数据结构定义成模板,所有字段都预先声明,从根上规避这个问题。
6. 进阶:把流程图接进业务系统——序列化、校验与性能
6.1 一份图对应一份快照:后端该存什么
流程图进入业务系统后,保存是第一关。只存 nodes 和 edges 不够,用户画到一半的缩放比例、平移位置也应该存,否则下次打开图是乱的。我常用的快照结构是这样:
const snapshot = { version: 1, viewport: { zoom: 1, pan: { x: 0, y: 0 } }, nodes: this.nodes, edges: this.edges }保存前做一轮校验,检查孤立节点、空连线、重复 id,校验不通过直接阻断保存并提示用户。加载时按 version 做兼容处理,后续字段调整时可以平滑迁移。
6.2 大图场景:几百节点如何保持流畅
超过两百个节点的图,交互会有可感知的卡顿。此时优先做三件事:节点数据瘦身,把长文本和大对象从 data 字段挪出去,只保留渲染必需字段;关闭非核心动画,减少连线路径重算的频率;配合画布虚拟滚动,只渲染视口内的节点。某次接一个大型组织架构图,我把自定义节点里的图表组件改为纯文本后,拖拽帧率翻了一倍。从那以后,每次对图做增删改,我都强制走一遍「改数据、校验、生成快照」的流程,确保图上和图下永远一致,再也没出现线上反馈的图画对不上数据的诡异事故。希望帮到你。
本文还有配套的精品资源,点击获取