资讯动态

Vuex + antv/g6:从拖拽到数据流的流程图编辑器实现方案

发布时间:2026/10/18 12:46:56 来源:尧图企业网站定制
简介基于 Vue 与 AntV G6 的流程图绘制演示 Demo面向需要在线绘图、拓扑分析或可视化建模的前端开发者以简洁工程还原流程图创建、节点链接、节点删除、菜单操作、重新渲染和本地存储等核心交互。资源共 93 个文件主要由 38 个 JavaScript 文件、34 个 Vue 组件、5 个 SCSS 样式文件构成同时含 mock 数据与标准构建配置压缩包仅 189KB结构紧凑、易读易改。已有 12821 人学习/下载热度较高。整体工程包含 src 源码、store 状态管理、router 路由、build 脚本和静态资源目录读者可直接运行查看效果也可借鉴 G6 与 Vue 的集成方式快速搭建可交互的分析流程图原型描述中未包含复杂业务算法便于把注意力放在 G6 基础能力与前端工程组织上适合有类似需求的中高级前端开发者。1. 从拖拽到数据流用Vuex和antv/g6做流程图编辑器我之前接手过一个内部工具要在网页上画业务分析流程图一开始直接拿 antv/g6 塞进组件里数据全放在data()中。结果不到两周就发现不对劲好几个页面要共享同一张图有的要看着当前选中节点有的要编辑节点属性还有的要按节点状态做统计。组件间靠$emit传事件传到我头皮发麻撤销重做更是没法写。后来把数据层整体迁到 VuexG6 只负责渲染和交互画布所有变更都通过store.commit和store.dispatch走统一通道这个Demo就是这套思路的浓缩版一个基于 Vue Vuex antv/g6 的最小在线流程图编辑器支持从工具栏拖节点、拖边连线、编辑节点名称、删除节点和边以及把画布数据序列化保存。适合正在用 G6 做可视化设计器、或者想搞明白「Vuex 到底怎么管理图形状态」的前端开发。对于只会把图写死在组件里的场景这个Demo能直接给出可抄的状态模型。2. 整体设计Vuex状态模型与G6实例的生命周期2.1 为什么用Vuex管理图数据而不是组件局部stateG6 的图结构本质上就是nodes: []和edges: []两个数组外加每个元素的属性字段。这种数据结构放进 Vuex 的state里非常自然。组件里不再保存任何节点数据只保存 G6 实例本身画布上每一次增删改都通过 action 修改 store再调用实例的graph.changeData()或者addItem同步到渲染层。这样带来的第一个好处是多组件可以共享同一份数据。比如左侧节点列表、右侧属性面板、顶部统计条它们读的都是同一个flow.nodes不需要跨组件传参。第二个好处是业务逻辑可以集中。比如删除节点时必须连带删除与它相连的所有边这种规则写在 action 里任何入口调用都能保证行为一致不会出现某条边成了“孤儿”。Vuex 的 state 必须是可序列化的G6 默认的节点数据只包含id、label、x、y等基础字段恰好满足要求。需要注意G6 实例本身不要放进 Vuex因为实例里有大量不可序列化的方法、内部事件对象放进 state 会让 Vuex DevTools 直接卡死。正确做法是 G6 实例保存在组件的data或者一个独立的非响应式模块里节点数据才进 store。这个Demo里我把graph实例放在FlowEditor.vue的data()中用Object.freeze包了一层防止 Vue 对它做响应式劫持。2.2 目录结构与职责划分拿到 Demo 源码后先看目录结构不大但每一层职责分得很清楚。以下是我在本地解包后整理出的骨架├── package.json ├── vue.config.js ├── public/ │ ├── index.html │ └── favicon.ico └── src/ ├── main.js ├── App.vue ├── store/ │ ├── index.js │ └── modules/ │ └── flow.js ├── components/ │ ├── FlowEditor.vue │ ├── ToolBar.vue │ ├── NodePanel.vue │ └── PropertyPanel.vue └── views/ └── Home.vuestore/modules/flow.js是核心里面定义statenodes、edges、history、mutations和actions。FlowEditor.vue负责创建 G6 实例、注册交互行为、把画布事件拖拽、点击、连线转发给 Vuex。NodePanel.vue提供了可拖拽的节点类型列表拖到画布上生成新节点。ToolBar.vue放撤销、重做、清空、保存按钮。PropertyPanel.vue用来编辑当前选中节点的名称、颜色等属性。Home.vue只是把这些组件拼到同一张页面里。这样的拆分逻辑是组件只管「界面长什么样」和「触发什么动作」至于流程图数据怎么变、变完以后怎么联动全部收口到 store。如果以后要加“多标签页同时开几张图”只需要把flow.js改成createFlowStore()工厂函数按标签页实例化多个模块就行。2.3 G6实例的创建与销毁从Vuejs挂载到Vuex初始化G6 实例必须在 DOM 渲染完成后才能创建所以一般放在mounted()里。以下是FlowEditor.vue中创建图实例的代码this.graph被挂到组件上图数据不会走响应式import G6 from antv/g6; import { mapActions } from vuex; export default { name: FlowEditor, data() { return { graph: null, containerRef: null, }; }, mounted() { this.initGraph(); this.loadInitialData(); }, beforeDestroy() { if (this.graph) { this.graph.destroy(); this.graph null; } }, methods: { ...mapActions(flow, [loadInitialData, addNode]), initGraph() { const container this.$refs.canvasContainer; const width container.clientWidth; const height container.clientHeight; this.graph new G6.Graph({ container, width, height, fitView: true, modes: { default: [drag-canvas, zoom-canvas, drag-node, click-select], }, defaultNode: { type: rect, style: { width: 120, height: 48, fill: #E8F4FF, stroke: #5B8FF9, radius: 4, }, labelCfg: { style: { fontSize: 14 } }, }, defaultEdge: { type: cubic-horizontal, style: { stroke: #87909E, lineWidth: 1.5 }, }, }); // 监听节点点击事件选中后把节点id写入Vuex this.graph.on(node:click, (evt) { const node evt.item.getModel(); this.$store.commit(flow/setSelectedNode, node.id); }); }, }, };这段代码最关键的参数是modes和defaultNode。modes.default数组里的drag-node负责节点拖拽click-select负责点击选中zoom-canvas和drag-canvas是画布缩放与平移演示在线分析流程时很实用。fitView: true会让整个图自动适配画布尺寸避免一开始节点太小。G6 实例创建后立刻绑定一个node:click事件这是连接 Vuex 的第一个口子用户点击节点组件把节点的id提交到 store属性面板就能响应式地展示节点信息。beforeDestroy里必须调用graph.destroy()否则切路由时画布容器被移除但 G6 的事件监听还在容易内存泄漏重新进入页面还会出现“canvas already exists”的报错。3. 核心模块拆解拖拽建节点、连线交互与Vuex同步链路3.1 从工具栏拖拽新节点到画布在线画流程图的第一步是在画布上放置节点。NodePanel.vue里每一种节点类型都是一个可拖拽的卡片拖拽时把节点类型信息挂在dataTransfer上画布区域监听drop事件拿到这个信息组装成 G6 节点数据并派发addNodeaction。// NodePanel.vue 中的拖拽开始事件 onDragStart(event, nodeType) { event.dataTransfer.setData(application/flow-node-type, nodeType); event.dataTransfer.effectAllowed move; } // FlowEditor.vue 中的 conDainer 监听 handleDrop(event) { event.preventDefault(); const nodeType event.dataTransfer.getData(application/flow-node-type); if (!nodeType) return; const canvasRect this.$refs.canvasContainer.getBoundingClientRect(); const x event.clientX - canvasRect.left; const y event.clientY - canvasRect.top; this.addNode({ id: node_${Date.now()}, type: nodeType, label: nodeType start ? 开始 : 处理, x, y, }); }这里的dataTransfer是浏览器原生拖放 API 的传参通道application/flow-node-type是自定义 MIME 类型避免和系统内部拖拽冲突。计算x和y时用了getBoundingClientRect()把鼠标相对页面的坐标转换成相对画布容器左上角的坐标这样才能让节点出现在鼠标落点的正确位置。节点 id 用Date.now()生成在单机Demo里不会重复但生产环境最好换用 uuid。addNode这个 action 在store/modules/flow.js里会往数组 push 这条记录然后FlowEditor通过watchstore 里的nodes变化调用graph.addItem()把节点真正画出来。3.2 节点拖拽和边连接的交互配置节点放上去之后要能拖动位置。G6 内置的drag-node行为会自动处理但拖完以后坐标变更必须同步回 Vuex否则刷新页面时节点会回到旧位置。我在node:dragend事件里更新 storegraph.on(node:dragend, (evt) { const model evt.item.getModel(); this.$store.commit(flow/updateNodePosition, { id: model.id, x: model.x, y: model.y, }); });连线交互是另一个关键。G6 中创建边有几种方式最简单的是在modes里加一个add-edge行为把defaultEdge的类型配好然后让用户从一个节点拖到另一个节点modes: { default: [drag-canvas, zoom-canvas, drag-node, click-select], addingEdge: [add-edge, drag-canvas], },在 ToolBar 里加一个「连线模式」按钮点击后调用graph.setMode(addingEdge)用户就可以拖边。边创建完成后 G6 会触发edge:add事件此时需要获取边的source和target节点 id提交给 Vuexgraph.on(edge:add, (evt) { const edge evt.item.getModel(); this.$store.commit(flow/commitEdge, { id: edge.id, source: edge.source, target: edge.target, }); });这里的edge是 G6 生成的 edge 模型source和target对应两端节点 id。注意commitEdge时还要检查重复边比如两个节点之间不允许画两条同方向边否则后续做流程分析时会出现重复路径。我在 store 的 mutation 里做了查重遍历已有 edges如果source和target都相同就返回并提示用户。这种业务规则放 mutation 里虽然有点“重”但能确保所有入口工具栏、右键菜单、脚本都在同一条保护线上。3.3 Vuex action处理增删改以删除节点为例删除节点是最容易出 bug 的操作因为它牵扯到边。如果只删掉节点不删关联边G6 渲染时会因为找不到节点而报错画布直接白屏。我在 Vuex 的 action 里统一处理这个逻辑// store/modules/flow.js actions: { deleteNode({ commit }, nodeId) { commit(deleteRelatedEdges, nodeId); commit(deleteNode, nodeId); }, }, mutations: { deleteRelatedEdges(state, nodeId) { state.edges state.edges.filter( (edge) edge.source ! nodeId edge.target ! nodeId ); }, deleteNode(state, nodeId) { state.nodes state.nodes.filter((node) node.id ! nodeId); state.selectedNodeId null; }, },组件里调用时只需要这样graph.on(node:delete, (evt) { const nodeId evt.item.getModel().id; this.deleteNode(nodeId); });删除节点时首先过滤掉所有以该节点为起点或终点的边然后再从state.nodes里移除节点最后清空选中状态。这样 Vuex 里的数据永远是干净的。组件里用graph.removeItem还是直接changeData我建议删除操作直接用graph.removeItem(item)性能更好但如果一次删除多个节点直接用graph.changeData({ nodes, edges })会更省心。这个Demo的 ToolBar 里有「清空画布」按钮就是先store.commit(clearFlow)再graph.changeData({ nodes: [], edges: [] })完成的。4. 运行与调试本地跑通Demo的完整步骤4.1 环境准备与依赖安装这个 Demo 是基于 Vue CLI 搭建的运行前需要确认本机有 Node.js 环境。我本地的 Node 版本是 16.xnpm 版本是 8.x没有特殊要求但建议不要低于 Node 12否则新版 G6 的依赖可能装不上。项目依赖都写在package.json里安装前先看一下核心依赖版本{ dependencies: { antv/g6: ^4.8.0, vue: ^2.6.14, vuex: ^3.6.2 } }这里用的是 Vue 2 Vuex 3因为 Vuex 4 是专门给 Vue 3 用的如果拿 Vue 3 去配 Vuex 3 会出兼容问题。打开终端进入项目根目录先执行依赖安装npm install如果网络环境不好或者之前装过其他项目导致 node_modules 状态混乱可以删掉node_modules和package-lock.json再重装rm -rf node_modules package-lock.json npm install --registryhttps://registry.npmmirror.com--registry参数把 npm 源切到了大陆镜像下载 G6 这种体积较大的包会快很多。安装完成后可以顺便跑一次npm audit看看依赖有没有高危漏洞不过Demo项目一般问题不大。4.2 启动开发服务器与验证功能安装完成后的启动命令很简单npm run serve默认监听localhost:8080如果端口被占用会提示你选8081。打开浏览器进入页面后先看浏览器控制台有没有红色报错重点看有没有「Container not found」或者「canvas not found」这类错误出现这个多半是 G6 容器 div 没有渲染出来或者容器高度为 0。正常界面应该是一块纯色画布左侧是节点类型列表上方是工具栏。验证功能时按这个顺序走一遍第一步从左侧拖一个「开始」节点到画布中间应该出现一个浅蓝色矩形第二步在左侧拖一个「处理」节点到下方两个节点间拖出连线第三步点击画布空白处取消选中再点击「处理」节点右侧属性面板应该出现该节点的 id 和 label第四步点工具栏里的「保存」刷新页面数据应该还在。保存功能我用的是 localStorage把整个nodes和edges存进了localStorage的flow_data键里刷新后重新读取。4.3 数据持久化把画布存到localStorage持久化在这个Demo里并不是必须的但为了演示“在线分析流程”的场景我加了简单的存/取。当用户点击保存时。// 在 FlowEditor.vue 中 saveToLocal() { const data { nodes: this.$store.state.flow.nodes, edges: this.$store.state.flow.edges, }; localStorage.setItem(flow_data, JSON.stringify(data)); this.$message.success(保存成功); }读取时在mounted里调用const saved localStorage.getItem(flow_data); if (saved) { const data JSON.parse(saved); this.$store.commit(flow/setFlowData, { nodes: data.nodes, edges: data.edges, }); }从 localStorage 读出来的数据必须先提交到 Vuex再让组件去 watch 并graph.changeData()渲染。顺序不能反过来否则画布先渲染了旧数据Vuex 里的新数据一更新又会触发一次重复渲染导致节点闪烁。这个“先 store 后渲染”的顺序也是后面排查问题时的核心判断点。5. 避坑指南G6 Vuex联动的常见问题与排查5.1 高发异常画布白屏、节点重复、拖拽失灵现象一画布渲染出来是白屏控制台报错Cannot read property getBoundingClientRect。原因G6 初始化的容器宽度或高度为 0通常是父容器用了flex: 1但没设置overflow: hidden或者组件挂载时容器还没完成布局。解决在mounted里用this.$nextTick包一层再读取clientWidth和clientHeight同时给画布容器一个确定的最小高度比如min-height: 600px。现象二所有节点堆在画布左上角位置不在 drop 的落点。原因计算坐标时没有减去容器偏移。我最初直接用了event.clientX和event.clientY但 G6 内部用的是容器内相对坐标因此需要通过getBoundingClientRect()转换。解决参考 3.1 节的计算方式并且注意页面是否有横向滚动条有滚动条时还要加上scrollLeft的补偿。现象三删除节点后过一会儿节点又「复活」了。原因Vuex 里的nodes数组删掉了数据但 G6 的图形实例还保留着渲染项第二次调用graph.changeData()时G6 会拿 store 里的新数据做全量对比发现该节点不在列表里才移除。如果你用了graph.addItem/graph.removeItem这种增量更新不同步删掉 item 就会出现数据残留。解决删除节点时在 Vuex mutation 删除后立刻调用graph.removeItem(evt.item)或者直接changeData全量刷新。最好是始终以 Vuex 数据为唯一真相组件里的图形状态只做视图同步。5.2 交互状态无法同步到属性面板节点点击后属性面板没有反应通常是事件绑错了目标。G6 里node:click事件的evt.item.getModel()才是节点数据而evt.target可能是节点内部的 shape 元素。如果你操作evt.target.getModel()有些版本会返回undefined。解决统一用evt.item它是 G6 的 Item 实例包含完整的模型。另外点击空白区域要取消选中节点需要监听canvas:click不能只监听node:click否则空白处点击后面板还显示旧节点。事件绑定位置也要注意如果绑定在某个子组件里组件销毁后事件会丢。5.3 Vuex状态更新了但画布不刷新我在联调时遇到过最头疼的问题Vuex 里nodes数量变了graph却没新增节点。原因是我在组件里watch了一个嵌套数组但 watch 的是引用地址。G6 的addItem是显式调用的Vuex 数据变化并不会自动触发画布重绘。解决不要在 mutation 里直接state.nodes.push()然后期待画布跟上而是要在 mutation 之后由视图层监听某个计数器或者直接调用graph.read()。我这里的做法是mutation 里维护一个version字段每次增删改version组件 watchversion然后统一做graph.changeData({ nodes: state.nodes, edges: state.edges })。虽然全量刷新多一点开销但极大降低了状态不同步的概率。对节点数量几十个的流程图场景性能完全没问题。5.4 边连接不上或连线不到目标节点出现这种情况要先确认两点一是两个节点之间是否已经存在同方向边我加了查重逻辑后就不会有重复边二是目标节点是否被画布中其他节点挡住add-edge行为默认只在鼠标移动到节点上方时触发连接。如果从边的终点拖到空白处边会消失或变成自环。解决在需求上要求用户必须从一个节点拖到另一个节点不要从空白处开始拖。然后检查 G6 的defaultEdge是否设置了linkPoints没有设置的话节点上不会有连接锚点拖拽时会很难对准。建议在defaultNode中加上linkPoints: { top: true, right: true, bottom: true, left: true, size: 6, fill: #fff, stroke: #5B8FF9, }这样每个节点四个方向都有可视化的连接点连线的可用性会明显提升。6. 二次开发从Demo到业务系统的改造思路如果你要把这个Demo改造成真正的业务流程图编辑器第一件事不是加功能而是把「固定数据类型」变成「可扩展 schema」。当前nodes里只有id、label、x、y和type业务中肯定要加config字段比如审批节点要配置「审批人」条件节点要配置「判断表达式」。我的改造路径是这样的先给 Vuex 模块增加一个nodeSchemas字典把每种节点的默认配置存放好然后 G6 的defaultNode不再写死宽高而是根据node.type动态返回。画布最初的渲染数据也改成通过JSON.parse(JSON.stringify())深拷贝避免从 store 读引用时被 G6 内部修改污染。验证一个改动是否安全我习惯做三步自测一是在浏览器控制台手动 dispatch 一次addNode强制走一遍「Vuex 数据变化 → 组件 watch → graph.changeData」的链路确认不经过 Vue 组件也能驱动画布二是用 G6 自带的graph.save()导出 JSON对比前后两次保存结果看新增字段是否完整三是清空 localStorage 后在新浏览器会话里重新画一张图排除旧数据干扰。做完这三步再上线到业务系统。从那以后我每次改这种联动逻辑都强制走一遍“先手动调 action再 check 数据最后画布比对”的流程几乎没再遇到状态不同步的问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑