第一章:VueFlow 概述与环境搭建
1.1 什么是 VueFlow:核心概念与应用场景
| 概念名称 | 说明 | 注意事项 |
|---|
| VueFlow | 一个基于 Vue 3 和 TypeScript 的流程图/节点编辑器库,用于构建可交互的图编辑界面。 | 需运行在 Vue 3 + Vite 或 Vue CLI 5+ 环境,不支持 Vue 2。 |
| 节点(Node) | 图中的基本单元,代表一个操作、状态或数据处理模块,可包含输入/输出端口。 | 节点必须具有唯一 ID 和位置坐标(x, y),否则无法正确渲染。 |
| 边(Edge) | 连接两个节点的线,表示数据流、控制流或依赖关系。 | 边必须指定 source(源节点 ID)和 target(目标节点 ID),否则连接无效。 |
| 可响应式(Reactive) | 基于 Vue 的响应式系统,节点和边的变化会自动更新视图。 | 所有对图状态的修改应通过 VueFlow 提供的 API(如 setNodes),避免直接修改。 |
| 自定义渲染 | 支持使用 Vue 组件自定义节点外观和交互逻辑。 | 自定义节点需通过 defineNode 或组件注册方式引入,确保正确解析。 |
| 内置交互 | 支持拖拽节点、缩放画布、连接节点、选择元素等开箱即用的交互功能。 | 可通过配置项禁用特定交互(如 panOnDrag、zoomOnScroll)。 |
1.2 VueFlow 与其他流程图库的对比
| 对比维度 | VueFlow | X6 (by AntV) | react-flow (React 版) | GoJS |
|---|
| 技术栈 | Vue 3 + TypeScript | 纯 JavaScript,支持多框架封装 | React + TypeScript | 纯 JavaScript,商业库 |
| 响应式机制 | 深度集成 Vue 响应式系统,自动更新视图 | 手动调用 render 或 update | 基于 React state 更新 | 手动调用 model 更新 |
| 学习成本 | 对 Vue 开发者极低 | 中等,API 较复杂 | 对 React 开发者友好 | 高,API 复杂且文档较分散 |
| 自定义能力 | 支持 Vue 组件作为节点,高度灵活 | 支持自定义节点和行为 | 支持 React 组件节点 | 极强,支持完全自定义图形和行为 |
| 社区与生态 | 新兴库,社区活跃度中等 | 阿里 AntV 项目,生态完善,文档齐全 | 社区活跃,插件丰富 | 商业支持,文档完善但需授权 |
| 许可证 | MIT 开源 | MIT 开源 | MIT 开源 | 商业授权 |
| 适用场景 | Vue 项目中的工作流、可视化编辑器 | 中后台复杂图编辑、拓扑图 | React 项目流程图 | 企业级复杂图表、商业应用 |
| 注意事项 | 目前版本迭代较快,API 可能有变动 | 体积较大,功能繁多 | 与 Vue 项目不兼容 | 需付费授权,不适合开源项目 |
1.3 开发环境准备与项目初始化
| 概念名称 | 说明 | 注意事项 |
|---|
| Node.js | JavaScript 运行环境,VueFlow 项目依赖 npm 或 yarn 进行包管理。 | 建议使用 LTS 版本(如 18.x 或 20.x),避免兼容性问题。 |
| npm / yarn | 包管理工具,用于安装 VueFlow 及其依赖。 | 推荐使用 yarn 或 pnpm 以获得更好的依赖解析性能。 |
| Vue 3 | 前端框架,VueFlow 基于其 Composition API 和响应式系统构建。 | 必须使用 Vue 3.2+,推荐使用 <script setup> 语法。 |
| Vite | 构建工具,提供快速启动和热更新体验。 | 初始化项目时推荐使用 vite@latest 创建 Vue + TypeScript 模板。 |
| TypeScript | 可选但推荐,VueFlow 提供完整的类型定义。 | 使用 TS 可提升开发体验和减少运行时错误。 |
| 浏览器支持 | 支持现代浏览器(Chrome, Firefox, Safari, Edge) | 不支持 IE11 及更低版本。 |
| 注意事项 | 确保本地已安装 Node.js 和包管理工具 | 初始化前检查 node -v 和 npm -v 是否正常输出版本号。 |
1.4 安装 VueFlow 及依赖管理
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| npm 安装 | npm install @vue-flow/core | 安装 VueFlow 核心库 | npm install @vue-flow/core | 必须安装此包才能使用基本功能 |
| yarn 安装 | yarn add @vue-flow/core | 使用 yarn 安装核心库 | yarn add @vue-flow/core | 推荐在已有 yarn 项目中使用 |
| pnpm 安装 | pnpm add @vue-flow/core | 使用 pnpm 安装核心库 | pnpm add @vue-flow/core | pnpm 用户推荐,节省磁盘空间 |
| 安装 TypeScript 支持 | npm install --save-dev typescript | 启用类型检查 | npm install --save-dev typescript | 若项目未启用 TS,需手动配置 tsconfig.json |
| 安装 Vue | npm install vue@^3.2 | 确保 Vue 3.2+ 已安装 | npm install vue@^3.2 | VueFlow 依赖 Vue 3.2+ 的响应式特性 |
| 引入样式文件 | import '@vue-flow/core/dist/style.css' | 导入 VueFlow 默认样式 | import '@vue-flow/core/dist/style.css' | 必须在入口文件(如 main.ts)中导入 |
| 引入基础样式 | import '@vue-flow/core/dist/theme.min.css' | 导入默认主题样式 | import '@vue-flow/core/dist/theme.min.css' | 可选,用于基础节点外观美化 |
| 检查版本 | npm list @vue-flow/core | 查看已安装的 VueFlow 版本 | npm list @vue-flow/core | 确保版本一致,避免因版本冲突导致问题 |
| 更新库 | npm update @vue-flow/core | 更新到最新兼容版本 | npm update @vue-flow/core | 更新前建议查看 CHANGELOG 避免 breaking change |
| 注意事项 | - | - | - | 安装后务必导入 CSS 文件,否则节点样式将失效 |
第二章:基础节点与边的构建
2.1 节点(Node)的基本结构与定义
| 概念名称 | 说明 | 注意事项 |
|---|
| id | 节点的唯一标识符,字符串类型,必须全局唯一。 | 若 ID 重复,可能导致渲染错误或状态更新异常。 |
| type | 节点类型,决定使用哪种内置或自定义组件渲染(如 ‘default’, ‘input’)。 | 未定义时默认为 ‘default’,自定义节点需提前注册。 |
| position | 节点在画布上的坐标,包含 x 和 y 数值。 | 坐标基于画布左上角,单位为像素,必须为数字。 |
| data | 存储节点业务数据的对象,可包含 label、value 等自定义字段。 | data 内容不会直接影响渲染,需在节点组件中手动使用。 |
| style | CSS 样式对象,用于覆盖默认样式。 | 推荐使用 className 控制样式,style 用于动态样式。 |
| className | 应用于节点外层容器的 CSS 类名,用于添加自定义样式。 | 可结合 Tailwind 或 SCSS 使用,支持多个类名(空格分隔)。 |
| draggable | 是否允许拖拽该节点(true / false)。 | 受画布配置 nodesDraggable 影响,优先级更高。 |
| selectable | 是否可被选中(true / false)。 | 影响 onNodeClick 等事件行为,false 时无法被点击选中。 |
| deletable | 是否可被删除(true / false)。 | 在启用删除功能的编辑器中控制节点删除权限。 |
| dragHandle | 拖拽手柄的 CSS 选择器,仅该区域可触发拖拽。 | 如 ‘.drag-handle’,用于实现局部拖拽(如仅标题栏可拖动)。 |
| sourcePosition | 输出连接点的位置(‘top’, ‘right’, ‘bottom’, ‘left’)。 | 用于自动连接布局,需与 Handle 组件配合使用。 |
| targetPosition | 输入连接点的位置(‘top’, ‘right’, ‘bottom’, ‘left’)。 | 同上,用于指定连接方向。 |
| hidden | 是否隐藏节点(true / false)。 | 隐藏后节点仍存在于状态中,但不渲染。 |
| width / height | 节点宽高(可选),用于布局计算。 | 某些布局算法(如 dagre)需要提供尺寸以避免重叠。 |
2.2 边(Edge)的基本结构与连接方式
| 概念名称 | 说明 | 注意事项 |
|---|
| id | 边的唯一标识符,字符串类型,必须全局唯一。 | 建议由系统生成(如使用 nanoid),避免手动指定导致冲突。 |
| source | 源节点 ID,表示边从哪个节点出发。 | 必须对应存在的节点 ID,否则边无法渲染。 |
| target | 目标节点 ID,表示边连接到哪个节点。 | 同上,必须有效。 |
| type | 边的类型,决定其外观和行为(‘default’, ‘smoothstep’, ‘step’, ‘straight’)。 | 默认为 ‘default’(直线),smoothstep 为圆角曲线。 |
| label | 边上的文本标签,显示在路径中间。 | 支持字符串或 JSX/组件,可用于显示连接条件或数据类型。 |
| labelStyle | 标签的 CSS 样式对象。 | 可设置字体大小、颜色等。 |
| labelShowBg | 是否显示标签背景(true / false)。 | 默认 true,背景用于提升可读性。 |
| labelBgStyle | 标签背景的样式对象。 | 可自定义背景颜色、圆角等。 |
| animated | 是否显示动画效果(true / false)。 | true 时边呈脉冲动画,常用于表示激活状态或数据流动。 |
| style | 边路径的 CSS 样式对象(如 stroke, strokeWidth)。 | 用于自定义颜色、线宽等。 |
| markerStart | 起始端箭头标记配置(如 { type: 'arrow', width: 20, height: 20 })。 | 较少使用,通常只在 target 端显示箭头。 |
| markerEnd | 结束端箭头标记配置,常用以表示方向。 | 默认为箭头,可自定义类型和尺寸。 |
| sourceHandle | 指定连接源节点的特定 Handle ID(用于多端口)。 | 需与节点内的 Handle 组件 id 对应。 |
| targetHandle | 指定连接目标节点的特定 Handle ID。 | 同上,实现精确端口连接。 |
| updatable | 是否允许用户拖拽修改连接(true / false 或 ‘start’/‘end’)。 | 控制边的可编辑性,false 时无法调整连接点。 |
| focusable | 是否可通过键盘聚焦(true / false)。 | 影响无障碍访问(a11y)支持。 |
| deletable | 是否可被删除(true / false)。 | 与节点 deletable 类似,控制删除权限。 |
2.3 使用 addNodes 和 addEdges 添加元素
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| addNodes | addNodes(nodes: Node[]) | 向当前图中批量添加一个或多个节点 | addNodes([{ id: 'n1', type: 'input', position: { x: 0, y: 0 }, data: { label: '开始' } }]) | 节点 ID 必须唯一,重复 ID 会导致静默失败或覆盖 |
| addEdges | addEdges(edges: Edge[]) | 向当前图中批量添加一个或多个边 | addEdges([{ id: 'e1', source: 'n1', target: 'n2' }]) | source 和 target 必须指向存在的节点 ID |
| addNodes (单个) | addNodes(node: Node) | 添加单个节点 | addNodes({ id: 'n2', type: 'default', position: { x: 100, y: 100 }, data: { label: '处理' } }) | 与数组形式兼容,推荐统一使用数组 |
| addEdges (单个) | addEdges(edge: Edge) | 添加单个边 | addEdges({ id: 'e2', source: 'n2', target: 'n3', animated: true }) | 单个边也可传入,内部自动转为数组处理 |
| 返回值 | void | 无返回值 | - | 操作为异步,状态更新后视图自动刷新 |
| 注意事项 | - | - | - | 必须在 useVueFlow() 返回的实例上调用 |
2.4 渲染初始流程图:initialElements 配置
| 概念名称 | 说明 | 注意事项 |
|---|
| initialElements | 初始化时传入的节点和边数组,格式为 (Node | Edge)[] | 必须在 useVueFlow() 或组件中作为配置项传入。 |
| 元素顺序 | 数组顺序影响渲染层级,后添加的元素在上层。 | 若有重叠,后定义的节点会覆盖前面的。 |
| ID 唯一性 | 所有元素的 id 必须唯一,包括节点和边。 | 建议使用前缀区分类型(如 ‘node-1’, ‘edge-1’)。 |
| 数据结构 | 支持混合节点和边在同一数组中定义。 | 示例:[ node1, node2, edge1 ] |
| 响应式更新 | initialElements 通常只在初始化时生效。 | 后续更新应使用 addNodes / setNodes 等 API。 |
| 与 setNodes 区别 | initialElements 用于首次加载,setNodes 用于运行时状态替换。 | 修改 initialElements 不会触发图更新,除非重新挂载组件。 |
| TypeScript 支持 | 可使用 Node 和 Edge 类型联合定义数组类型。 | 通过 import { Node, Edge } from '@vue-flow/core' 引入类型。 |
代码示例:
const initialElements = [
{ id: '1', type: 'input', position: { x: 0, y: 0 }, data: { label: 'Start' } },
{ id: '2', position: { x: 100, y: 100 }, data: { label: 'Process' } },
{ id: 'e1', source: '1', target: '2' }
]
第三章:状态管理与响应式数据
3.1 VueFlow 的核心状态:useVueFlow 详解
| 概念名称 | 说明 | 注意事项 |
|---|
| useVueFlow() | Composition API 钩子,用于访问 VueFlow 的状态和操作方法。 | 必须在 <script setup> 或 setup() 中调用,且组件需在 <VueFlow> 内部。 |
| nodes | 响应式数组,包含当前图中所有节点对象。 | 可通过 .value 访问,修改应使用 setNodes 而非直接赋值。 |
| edges | 响应式数组,包含当前图中所有边对象。 | 同上,直接修改 edges.value 不会触发视图更新。 |
| viewport | 响应式对象,包含缩放(zoom)和平移(x, y)信息。 | 可用于保存/恢复视图状态,如 fitView 后获取当前视口。 |
| getNodes() | 函数,返回当前节点数组(非响应式快照)。 | 用于读取节点状态,等价于 nodes.value。 |
| getEdges() | 函数,返回当前边数组(非响应式快照)。 | 等价于 edges.value。 |
| project | 函数,将客户端坐标转换为画布坐标。 | 用于在点击空白处添加节点时计算正确位置。 |
| fitView | 函数,自动缩放和平移以使所有元素可见。 | 可传入选项(如 padding, maxZoom)控制拟合效果。 |
| setViewport | 函数,手动设置 viewport 状态(x, y, zoom)。 | 用于实现自定义导航或恢复保存的视图。 |
| screenToFlow | 别名:project,将屏幕坐标转为画布坐标。 | 两者功能完全相同。 |
| flowToScreen | 将画布坐标转为屏幕坐标。 | 用于弹窗定位、工具提示等场景。 |
| 注意事项 | - | useVueFlow 必须在 VueFlow 组件内部调用,否则会抛出错误。 |
3.2 响应式更新节点与边:setNodes、setEdges
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| setNodes | setNodes(nodes: Node[] | ((nodes: Node[]) => Node[])) | 完全替换当前节点数组 | setNodes([{ id: 'n1', type: 'default', position: { x: 0, y: 0 }, data: { label: '更新' } }]) | 会删除所有原有节点,确保新数组包含全部所需节点 |
| setEdges | setEdges(edges: Edge[] | ((edges: Edge[]) => Edge[])) | 完全替换当前边数组 | setEdges([{ id: 'e1', source: 'n1', target: 'n2' }]) | 同上,会清除未包含的边 |
| setNodes (函数式) | setNodes(prev => [...prev, newNode]) | 基于前一状态更新节点 | setNodes(prev => prev.map(n => n.id === 'n1' ? { ...n, selected: true } : n)) | 推荐用于局部更新,避免状态丢失 |
| setEdges (函数式) | setEdges(prev => [...prev, newEdge]) | 基于前一状态更新边 | setEdges(prev => prev.filter(e => e.id !== 'e1')) | 同上,支持过滤、映射等操作 |
| 返回值 | void | 无返回值 | - | 操作为异步,状态更新后视图自动刷新 |
| 注意事项 | - | - | - | 不要直接修改 nodes.value 或 edges.value,必须使用 setNodes/setEdges |
3.3 获取当前图状态:toObject 方法
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| toObject | toObject(): { nodes: Node[], edges: Edge[], viewport: { x: number, y: number, zoom: number } } | 获取图的完整状态快照 | const snapshot = toObject() | 返回值不包含函数或组件,仅数据可序列化 |
| 返回值结构 | Object | 包含 nodes, edges, viewport | console.log(toObject()) | 可直接 JSON.stringify 用于保存或传输 |
| 序列化用途 | - | 用于保存工作流到数据库或本地存储 | localStorage.setItem('flow', JSON.stringify(toObject())) | 是实现持久化的关键方法 |
| 还原状态 | - | 结合 setNodes/setEdges 恢复图状态 | const saved = JSON.parse(localStorage.getItem('flow')); setNodes(saved.nodes); setEdges(saved.edges) | 需分别处理 nodes 和 edges 数组 |
| 注意事项 | - | - | - | toObject 不包含自定义组件实例,仅数据部分 |
3.4 监听图状态变化:onPaneReady 与 onError
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| onPaneReady | onPaneReady(callback: () => void) | 当画布首次渲染完成并准备好时触发 | onPaneReady(() => { console.log('画布已就绪') }) | 通常用于执行 fitView 或初始化操作 |
| onError | onError(callback: (error: string, data: any) => void) | 当 VueFlow 内部发生错误时调用 | onError((err, data) => { console.error('VueFlow 错误:', err, data) }) | 可用于错误上报或降级处理 |
| 执行时机 | 一次性执行 | 仅在画布挂载并初始化完成后触发一次 | onPaneReady(() => { fitView() }) | 不会重复触发,适合初始化逻辑 |
| 错误类型 | - | 可能包括渲染错误、连接错误等 | 常见 error 值:‘node-add-failed’, ‘edge-connect-invalid’ | data 参数包含相关上下文信息 |
| 清理监听 | - | 无直接清理方法,依赖组件卸载 | - | 若在组件卸载前需移除,暂无官方 API,建议避免内存泄漏 |
| 注意事项 | - | - | - | 必须在 setup 或 onMounted 中调用,确保监听器注册成功 |
第四章:交互操作与用户事件
4.1 节点的拖拽、缩放与移动控制
| 概念名称 | 说明 | 注意事项 |
|---|
| panOnDrag | 鼠标拖拽画布时是否平移视图(true / false 或鼠标按键)。 | 默认 true,设为 false 可禁用画布拖动。 |
| zoomOnScroll | 是否通过鼠标滚轮缩放视图(true / false)。 | 默认 true,false 时禁用滚动缩放。 |
| zoomOnPinch | 是否通过触摸板或触屏双指捏合缩放(true / false)。 | 默认 true,移动端重要。 |
| panOnScroll | 是否通过鼠标滚轮平移视图(需配合 ctrl 键)。 | 默认 false,开启后可能与 zoomOnScroll 冲突。 |
| preventScrolling | 是否阻止页面级滚动(当鼠标在画布上时)。 | 默认 true,避免画布操作导致页面滚动。 |
| translateExtent | 限制画布可平移的范围,格式为 [[minX, minY], [maxX, maxY]]。 | 设为 [[-∞,-∞], [∞,∞]] 表示无限制,默认限制在合理范围内。 |
| nodeOrigin | 节点的坐标原点,默认为 { x: 0, y: 0 }(左上角)。 | 可设为 { x: 0.5, y: 0.5 } 使坐标基于中心点。 |
| onlyRenderVisibleElements | 是否仅渲染可视区域内的节点和边(性能优化)。 | 默认 false,大数据量时建议开启以提升性能。 |
| noDragClassName | 添加该 CSS 类名的元素不会触发节点拖拽。 | 如 ‘no-drag’,用于节点内部可点击但不可拖的区域。 |
| noPanClassName | 添加该类名的元素不会触发画布平移。 | 如 ‘no-pan’,常用于节点上的按钮或输入框。 |
| autoPanOnNodeDrag | 拖拽节点到边缘时是否自动平移画布(true / false)。 | 默认 true,提升大图操作体验。 |
| minZoom / maxZoom | 最小和最大缩放级别(数值)。 | 默认 minZoom=0.5, maxZoom=2,可自定义限制。 |
4.2 节点选择与多选机制:onNodeClick、onNodeDoubleClick
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| onNodeClick | onNodeClick(callback: (node: Node, event: MouseEvent) => void) | 当节点被单击时触发 | onNodeClick((node) => { console.log('选中节点:', node.id) }) | 会覆盖默认选中行为,需手动调用 setSelectedNodes |
| onNodeDoubleClick | onNodeDoubleClick(callback: (node: Node, event: MouseEvent) => void) | 当节点被双击时触发 | onNodeDoubleClick((node) => { editNode(node) }) | 常用于打开编辑弹窗 |
| selectedNodes | 响应式数组,包含当前被选中的节点。 | 可通过 selectedNodes.value 访问 | 修改应使用 setSelectedNodes | |
| setSelectedNodes | setSelectedNodes(ids: string[]) | 批量设置选中节点(按 ID) | setSelectedNodes(['n1', 'n2']) | 会清除之前的选择 |
| addSelectedNodes | addSelectedNodes(ids: string[]) | 添加节点到当前选择集 | addSelectedNodes(['n3']) | 实现多选功能 |
| removeSelectedNodes | removeSelectedNodes(ids: string[]) | 从选择集中移除节点 | removeSelectedNodes(['n1']) | 配合 Ctrl/Cmd 多选使用 |
| fitViewOnSingleSelection | 点击单个节点时是否自动居中并放大(配置项) | 默认 false | - | |
| 注意事项 | - | - | - | onNodeClick 后若需保持选中,必须手动更新状态 |
4.3 边的创建与删除:connectionMode 与 connectOnClick
| 概念名称 | 说明 | 注意事项 |
|---|
| connectOnClick | 是否允许通过点击节点端口创建连接(true / false)。 | 默认 true,false 时只能通过 Handle 拖拽连接。 |
| connectionMode | 连接模式,‘strict’(严格)或 ‘loose’(宽松)。 | strict 要求 source 和 target 明确,loose 允许任意连接。 |
| connectionRadius | 判断是否连接成功的检测半径(像素)。 | 默认 50,值越大越容易连接,但可能误触。 |
| nodesConnectable | 全局控制节点是否可连接(true / false)。 | 可被单个节点的 connectable 属性覆盖。 |
| edgesFocusable | 边是否可聚焦(影响键盘导航)。 | 默认 true。 |
| edgesUpdatable | 边是否可被用户更新(true / false 或 ‘start’/‘end’)。 | 控制是否允许拖拽修改连接端点。 |
| edgesDeletable | 边是否可被删除(true / false)。 | 与 deletable 属性一致。 |
| deleteKeyCode | 删除选中元素的快捷键(如 ‘Backspace’ 或 ‘Delete’)。 | 可设为 null 禁用快捷键删除。 |
| createElementsOnConnect | 连接成功后是否自动创建边(true / false)。 | 默认 true,false 时需在 onConnect 中手动添加。 |
| handle | 用于定义连接端口的组件,需指定 type (‘source’/‘target’) 和 position。 | 必须作为节点子组件使用。 |
4.4 自定义交互事件:onConnect、onEdgeClick、onPaneClick
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| onConnect | onConnect(callback: (connection: Connection) => void) | 当成功建立连接时触发 | onConnect(({ source, target }) => { addEdges([{ id: genId(), source, target }]) }) | 需手动添加边,除非 createElementsOnConnect=true |
| onEdgeClick | onEdgeClick(callback: (edge: Edge, event: MouseEvent) => void) | 当边被点击时触发 | onEdgeClick((edge) => { console.log('连接详情:', edge) }) | 可用于查看连接属性或删除边 |
| onEdgeDoubleClick | onEdgeDoubleClick(callback: (edge: Edge, event: MouseEvent) => void) | 当边被双击时触发 | onEdgeDoubleClick((edge) => { editEdgeLabel(edge) }) | 常用于编辑边标签 |
| onPaneClick | onPaneClick(callback: (event: MouseEvent) => void) | 当画布空白处被点击时触发 | onPaneClick(() => { setSelectedNodes([]) }) | 常用于清空当前选择 |
| onPaneContextMenu | onPaneContextMenu(callback: (event: MouseEvent) => void) | 右键点击画布时触发 | onPaneContextMenu((e) => { showMenu(e.clientX, e.clientY) }) | 用于显示上下文菜单 |
| onMove | onMove(callback: (event: { viewport: Viewport }) => void) | 画布移动时持续触发 | onMove(({ viewport }) => { console.log('视口变化:', viewport) }) | 性能敏感,避免重计算 |
| onMoveEnd | onMoveEnd(callback: (viewport: Viewport) => void) | 画布移动结束后触发 | onMoveEnd((vp) => { saveViewport(vp) }) | 适合保存视图状态 |
| Connection 对象 | { source: string, target: string, sourceHandle?: string, targetHandle?: string } | onConnect 回调参数 | - | source 和 target 为节点 ID,handle 用于多端口 |
| 注意事项 | - | - | - | 所有事件监听器应在 setup 或 onMounted 中注册 |
第五章:节点类型与自定义节点
| 节点类型 | 说明 | 默认样式与行为 | 使用场景 | 注意事项 |
|---|
| default | 基础矩形节点,无特殊样式或行为。 | 白色背景,灰色边框,可拖拽、可选中,无内置端口。 | 通用处理节点、占位符 | 最常用类型,适合大多数自定义需求。 |
| input | 输入型节点,通常作为流程起点。 | 绿色左上角标记,视觉上突出其”开始”角色,行为与 default 相同。 | 流程起始点、触发器 | 仅视觉区分,逻辑需自行实现。 |
| output | 输出型节点,通常作为流程终点。 | 红色右下角标记,视觉上突出其”结束”角色。 | 流程终止点、结果输出 | 仅视觉区分,不强制连接规则。 |
| 共同点 | - | 均支持 data、style、className 等通用属性。 | - | 所有内置节点均可通过 addEdges 连接,无连接方向限制(除非使用 Handle)。 |
| 注意事项 | - | 内置类型不包含 Handle 组件,若需连接端口,必须手动添加或使用自定义节点。 | - | 若需精确控制连接方向,建议使用自定义节点并集成 Handle。 |
5.2 自定义节点组件的注册与使用
| 概念名称 | 说明 | 代码示例 | 注意事项 |
|---|
| 自定义组件 | Vue 组件,接收 data, selected, id 等 props,可自由渲染。 | <script setup> defineProps(['data', 'selected', 'id']) </script> <template> <div class="custom-node"> {{ data.label }} </div> </template> | 组件名建议语义化,如 CustomTaskNode。 |
| 注册方式 | 通过 nodeTypes 配置项注册自定义节点类型。 | const nodeTypes = { 'custom-task': CustomTaskNode } | nodeTypes 需传递给 <VueFlow :nodeTypes="nodeTypes" />。 |
| 使用方式 | 在 addNodes 或 initialElements 中设置 type: 'custom-task'。 | { id: 'n1', type: 'custom-task', data: { label: '任务' }, position: { x: 0, y: 0 } } | type 必须与 nodeTypes 中注册的键名完全匹配。 |
| 内置 Props | 组件自动接收:data, selected, id, type, isConnectable 等。 | <div :class="{ 'selected': selected }">{{ data.label }}</div> | data 包含用户传入的业务数据,selected 表示是否被选中。 |
| 样式控制 | 使用 className 或内联 style 控制外观。 | { className: 'bg-blue-100 border-2', style: { width: '200px' } } | 推荐使用 className 结合 Tailwind 或 SCSS。 |
| 注意事项 | - | - | 自定义组件必须在 <VueFlow> 内部使用 nodeTypes 注册,否则 fallback 到 default。 |
5.3 使用 defineNode 定义可复用节点
注意: defineNode 并非 Vue Flow 官方 API。官方模式为通过 nodeTypes 注册组件。以下为社区常见封装模式模拟。
| 概念名称 | 说明 | 代码示例(模拟模式) | 注意事项 |
|---|
| 封装函数 | 创建一个返回节点配置的工厂函数,实现复用。 | const createTaskNode = (id, position, label) => ({ id, type: 'custom-task', position, data: { label, status: 'pending' } }) | 非官方 API,但可提升代码可维护性。 |
| 类型集中管理 | 将节点类型定义集中在一个文件中导出。 | // nodes.js export const TaskNode = { type: 'task', component: TaskNodeComponent } | 便于大型项目维护。 |
| 配置合并 | 工厂函数支持默认值与用户配置合并。 | const createNode = (config) => ({ type: 'default', ...config }) | 实现灵活的节点创建。 |
| TypeScript 支持 | 为工厂函数添加类型提示。 | type NodeConfig = { id: string; label: string; x: number; y: number } | 提升开发体验。 |
| 注意事项 | - | - | Vue Flow 本身不提供 defineNode,需自行封装或使用第三方库。 |
5.4 动态节点类型分发:type 字段控制
| 概念名称 | 说明 | 代码示例 | 注意事项 |
|---|
| type 字段 | 节点的 type 属性决定渲染哪个组件(内置或注册的自定义组件)。 | { id: 'n1', type: 'decision', ... } | type 是类型分发的核心,必须与 nodeTypes 中的键名一致。 |
| 条件渲染 | 在自定义组件内部根据 data.type 或其他字段动态渲染不同 UI。 | <template> <div v-if="data.kind === 'approval'">审批</div> <div v-else>普通任务</div> </template> | 适合同一组件承载多种子类型。 |
| 多态节点 | 一个自定义组件支持多种业务形态,通过 data 驱动。 | { type: 'card-node', data: { kind: 'error', title: '失败' } } | 减少组件数量,提升复用性。 |
| 运行时切换 | 动态修改节点的 type 字段可切换其渲染形态。 | setNodes(prev => prev.map(n => n.id === 'n1' ? { ...n, type: 'success' } : n)) | 新 type 必须已注册,否则渲染失败。 |
| 错误处理 | 未注册的 type 会 fallback 到 default 节点。 | { type: 'non-existent' } → 渲染为 default 节点 | 建议在开发环境打印警告。 |
| 最佳实践 | - | 1. 使用语义化 type 名(如 user-task, api-call) 2. 集中管理 nodeTypes 3. 避免频繁切换 type | - |
第六章:边的高级配置与连接规则
6.1 边的类型:default、smoothstep、step、straight
| 边类型 (type) | 路径样式说明 | 适用场景 | 代码示例 | 注意事项 |
|---|
| default | 曲线(贝塞尔曲线),平滑连接,带轻微弧度。 | 通用连接,视觉友好 | { id: 'e1', source: 'n1', target: 'n2', type: 'default' } | 默认类型,适合大多数流程图。 |
| smoothstep | 直角圆弧过渡,路径为水平→垂直或垂直→水平,转角圆润。 | 状态机、流程步骤图,强调方向性 | { type: 'smoothstep', style: { stroke: '#6366f1' } } | 避免交叉时清晰易读,常用于 BPMN 风格图。 |
| step | 直角硬转折,路径由纯水平和垂直线段组成,无圆角。 | 电路图、数据流图,追求精确布局 | { type: 'step', animated: true } | 视觉更”机械”,适合技术架构图。 |
| straight | 直线连接,两点间最短路径。 | 简单关系图、网络拓扑,强调直接关联 | { type: 'straight', markerEnd: 'arrow' } | 大量节点时可能产生视觉混乱,需合理布局。 |
| 自定义类型 | 可通过 edgeTypes 注册自定义渲染组件(如 elbow, curve)。 | 特殊可视化需求 | 注册后使用 type: 'custom-edge' | 需实现 SVG 或 Canvas 渲染逻辑。 |
| 注意事项 | - | - | - | 类型必须小写且与注册名一致;未注册类型会 fallback 到 default。 |
6.2 标签与箭头:markerEnd、label 配置
| 属性名称 | 说明 | 取值/语法 | 代码示例 | 注意事项 |
|---|
| label | 在边上显示文本标签。 | string 或 number | { label: '确认通过' } | 标签默认居中,可通过 labelStyle 和 labelX, labelY 调整位置。 |
| labelStyle | 标签的 CSS 样式对象。 | { fontSize: 12, fill: '#333' } | { label: 'API调用', labelStyle: { fontWeight: 'bold' } } | 支持标准 SVG 文本属性。 |
| labelShowBg | 是否显示标签背景框。 | boolean(默认 true) | { label: '分支', labelShowBg: false } | 关闭后标签更轻量,但可读性可能下降。 |
| labelBgStyle | 标签背景框样式。 | CSS 样式对象 | { labelBgStyle: { fill: 'yellow', rx: 2 } } | rx 控制圆角半径。 |
| labelBgPadding | 标签背景内边距。 | { x: number, y: number } | { labelBgPadding: { x: 4, y: 2 } } | 微调标签与背景的间距。 |
| markerEnd | 定义边终点的箭头标记。 | ‘arrow’, ‘arrowclosed’,或自定义 ID | { markerEnd: 'arrow' } 或 { markerEnd: { type: 'arrow', color: 'red' } } | arrowclosed 为实心箭头。 |
| markerStart | 定义边起点的箭头标记(较少用)。 | 同 markerEnd | { markerStart: 'circle' } | 可用于双向流程或特殊语义。 |
| 自定义 Marker | 通过 <Marker> 组件在 SVG 中定义复杂箭头。 | 在模板中声明 <defs><marker id="custom">...</marker></defs> | 使用 markerEnd: 'url(#custom)' 引用 | 高级用法,适合品牌化设计。 |
| 注意事项 | - | - | - | markerEnd 若为字符串,则使用内置样式;若为对象,可覆盖颜色、宽度等。 |
6.3 连接策略控制:isValidConnection
| 概念名称 | 说明 | 代码示例 | 注意事项 |
|---|
| isValidConnection | 函数,用于在连接前校验是否允许建立连接。 | isValidConnection: (connection) => { return connection.source !== connection.target // 禁止自环 } | 返回 true 允许,false 拒绝。 |
| 参数 connection | 包含 source, target, sourceHandle, targetHandle 的对象。 | (conn) => conn.source?.startsWith('out') && conn.target?.startsWith('in') | 可结合 Handle 的 id 或 type 实现精细控制。 |
| 应用场景 | - 禁止自环连接 | isValidConnection: ({ source, target }) => getNodeType(source) !== 'end' && getNodeType(target) !== 'start' | 常用于防止无效流程。 |
| - 限制输入/输出端口匹配 | | |
| - 类型系统校验(如 A 类节点不能连 B 类) | | |
| 与 connectionMode 协同 | strict 模式下更依赖 isValidConnection 进行验证。 | - | loose 模式下仍可被此函数拦截。 |
| 动态反馈 | 结合 onConnect 实现用户提示(如 toast 提示”连接不合法”)。 | onConnect: (conn) => { if (!isValid(conn)) showToast('类型不匹配!') } | 提升用户体验。 |
| 注意事项 | - | - | 函数应在组件初始化时定义,避免每次渲染重建;返回 false 时连接操作被取消。 |
6.4 多端口连接与 Handle 组件使用
| 概念名称 | 说明 | 代码示例(Vue 组件内) | 注意事项 |
|---|
| Handle 组件 | Vue Flow 提供的端口组件,用于定义连接点。 | <Handle type="source" position="right" id="output" /> | 必须作为自定义节点的子组件使用。 |
| type | 端口类型,‘source’(输出)或 ‘target’(输入)。 | <Handle type="target" ... /> | 决定连接方向,source 只能拖出,target 只能接收。 |
| position | 端口在节点上的位置:‘top’, ‘bottom’, ‘left’, ‘right’。 | <Handle position="top" ... /> | 影响边的起始方向,建议与布局匹配。 |
| id | 端口唯一标识符,用于多端口场景区分。 | <Handle id="price-out" type="source" ... /> | 当节点有多个同方向端口时必须设置,否则默认共享连接。 |
| isConnectable | 是否可连接(true/false 或数值控制连接数)。 | <Handle :isConnectable="canConnect" ... /> | 可动态绑定,实现运行时禁用端口。 |
| 多端口示例 | 一个节点有多个输入/输出端口。 | | |
<template>
<div class="node">
<Handle type="target" position="left" id="in1" />
<Handle type="source" position="right" id="out1" />
<Handle type="source" position="right" id="out2" />
</div>
</template>
| 连接规则 | isValidConnection 可基于 Handle.id 控制。 | isValidConnection: ({ sourceHandle, targetHandle }) => sourceHandle === 'out1' && targetHandle === 'in1' | 实现复杂的数据流或信号路由。 |
| 注意事项 | - | 1. Handle 不可见时仍可连接,建议用 className=“hidden” 隐藏 2. 确保 id 全局唯一或在节点内唯一 3. 使用 style 或 className 自定义端口外观 | - |
第七章:布局算法与自动排布
7.1 手动布局 vs 自动布局
| 对比维度 | 手动布局 | 自动布局 | 适用场景 |
|---|
| 控制粒度 | 完全由开发者或用户通过拖拽设置每个节点的 position: { x, y }。 | 由算法根据节点关系自动计算坐标。 | 手动:小型图、精确排版;自动:大型图、动态数据。 |
| 实现方式 | 在 initialElements 或 addNodes 中直接指定 position。 | 调用布局算法(如 dagre)处理节点与边,生成新坐标后更新节点。 | - |
| 灵活性 | 高,可任意摆放,支持复杂视觉设计。 | 受算法约束,布局模式固定(如树状、环形)。 | 手动适合设计稿还原,自动适合标准化流程。 |
| 维护成本 | 节点增删时需手动调整位置,易错且耗时。 | 增删节点后重新运行算法即可,维护成本低。 | 数据频繁变动时自动布局优势明显。 |
| 性能 | 渲染快,无计算开销。 | 初始布局有计算延迟,尤其节点数 > 100 时。 | 大图建议异步执行或添加加载状态。 |
| 用户交互 | 用户可通过拖拽自由调整,位置持久化需自行保存。 | 用户调整后可能被下次布局重置,需提供”锁定位置”功能。 | 可结合:自动布局初始化 + 手动微调。 |
| 代码示例 | { id: 'n1', position: { x: 100, y: 200 }, ... } | const layoutedElements = applyDagre(elements); setNodes(layoutedElements.nodes); | - |
| 注意事项 | - | - | 混合使用时,避免自动布局覆盖用户手动调整的位置。 |
7.2 集成 dagre 实现层级布局
| 步骤 | 说明 | 代码示例 | 注意事项 |
|---|
| 安装 dagre | 安装布局引擎库。 | npm install dagre | Vue Flow 不内置 dagre,需单独安装。 |
| 准备数据 | 收集当前节点和边(nodes 和 edges)。 | const elements = getElements(); | 确保节点有唯一 id,边有 source 和 target。 |
| 创建图实例 | 初始化 dagre 图并设置方向、间距等。 | | |
import dagre from 'dagre';
const g = new dagre.graphlib.Graph();
g.setGraph({ rankdir: 'TB', marginx: 10, marginy: 10 });
g.setDefaultEdgeLabel(() => ({}));
| 添加节点 | 将每个节点添加到图中,设置宽度和高度。 | nodes.forEach(node => { g.setNode(node.id, { width: 180, height: 36 }); }); | 尺寸影响布局紧凑度,建议与实际渲染尺寸一致。 |
| 添加边 | 将每条边添加到图中。 | edges.forEach(edge => { g.setEdge(edge.source, edge.target); }); | dagre 根据边关系确定层级。 |
| 执行布局 | 调用 dagre 布局算法计算坐标。 | dagre.layout(g); | 计算完成后,节点位置写入 g.node(nodeId)。 |
| 更新位置 | 将 dagre 计算的坐标同步到 Vue Flow 节点。 |
setNodes(nodes.map(node => ({
...node,
position: {
x: g.node(node.id).x - node.width / 2,
y: g.node(node.id).y - node.height / 2
}
})));
| 调整视图 | 布局后调用 fitView 让图居中显示。 | fitView(); | 提升用户体验,避免图在视野外。 |
| 完整封装 | 将上述逻辑封装为 useDagreLayout Hook 或函数。 | const applyDagreLayout = (elements, options) => { ... } | 便于多处复用。 |
| 注意事项 | - | 1. 处理异步:大图可使用 setTimeout 防卡顿 2. 错误边界:捕获 dagre 可能的异常 | - |
7.3 使用 fitView 与 zoomTo 调整视图
| 方法/属性 | 语法与说明 | 代码示例 | 注意事项 |
|---|
| fitView | 调整缩放和平移,使所有元素或指定区域适配画布。 | fitView(); // 全图适配 fitView({ nodes: ['n1', 'n2'], padding: 0.2 }); // 指定节点 | 默认有 padding,可传 minZoom, maxZoom 限制缩放级别。 |
| zoomTo | 将视图缩放到指定比例(1.0 = 100%)。 | zoomTo(0.5); // 缩小到 50% | 不改变位置,仅缩放。 |
| zoomIn | 放大(通常 +10%)。 | zoomIn(); | 可绑定快捷键或按钮。 |
| zoomOut | 缩小(通常 -10%)。 | zoomOut(); | - |
| setTransform | 同时设置 x, y, zoom。 | setTransform({ x: 100, y: 50, zoom: 1.2 }); | 用于恢复保存的视图状态。 |
| getViewport | 获取当前视口状态 { x, y, zoom }。 | const vp = getViewport(); console.log(vp.zoom); | 可用于持久化用户视角。 |
| 常见组合 | 布局后自动适配。 | applyDagreLayout(); setTimeout(() => fitView({ minZoom: 0.1 }), 100); | 异步执行避免与布局渲染冲突。 |
| 动画控制 | fitView 和 setTransform 支持 duration 参数实现平滑动画。 | fitView({ duration: 500 }); | 提升视觉流畅度。 |
| 注意事项 | - | 1. fitView 在元素未渲染时可能失效,确保在 nextTick 或 onMounted 后调用 2. 频繁调用可能影响性能 | - |
7.4 自定义布局算法接入
| 步骤 | 说明 | 代码示例(模拟环形布局) | 注意事项 |
|---|
| 选择算法 | 选择或实现布局算法(如力导向、环形、树状、网格)。 | - | 可使用 d3-force、cola.js 等库。 |
| 定义函数 | 创建函数接收 nodes 和 edges,返回带新 position 的节点数组。 | | |
const circularLayout = (nodes, centerX = 400, centerY = 300, radius = 200) => {
return nodes.map((node, i) => {
const angle = (i / nodes.length) * 2 * Math.PI;
return {
...node,
position: {
x: centerX + radius * Math.cos(angle),
y: centerY + radius * Math.sin(angle)
}
};
});
};
| 集成到 Vue Flow | 在用户触发时调用算法并更新节点位置。 | const layoutedNodes = circularLayout(getNodes()); setNodes(layoutedNodes); fitView(); | 避免直接修改原数组,返回新引用触发响应式更新。 |
| 参数配置 | 支持传入参数(如半径、方向、间距)以增强灵活性。 | circularLayout(nodes, 500, 500, 300); | 可通过 UI 控件(滑块、下拉)调整参数。 |
| 性能优化 | 大图时使用 Web Worker 避免阻塞主线程。 | 将算法逻辑放入 Worker,通过 postMessage 通信。 | 复杂算法(如力导向)推荐此方式。 |
| 与内置功能协同 | 结合 fitView、过渡动画等提升体验。 | - | 布局后自动 fitView 是标准实践。 |
| 错误处理 | 捕获算法异常,提供 fallback(如退回手动布局)。 | try { applyCustomLayout(); } catch (err) { console.error('布局失败:', err); } | 确保系统稳定性。 |
| 注意事项 | - | 1. 算法输出必须是新对象数组,否则 Vue 无法检测变化 2. 考虑节点尺寸,避免重叠 3. 提供”重置布局”功能 | - |
第八章:进阶状态控制与插件系统
8.1 使用 useStore 直接操作内部状态
| 概念 | 说明 | 代码示例 | 注意事项 |
|---|
| useStore | Vue Flow 提供的 Hook,返回可响应式访问和修改画布内部状态的对象。 | import { useStore } from '@vue-flow/core'; const store = useStore(); console.log(store.nodes); // 获取当前所有节点 | 直接操作底层状态,需谨慎使用,避免破坏内部一致性。 |
| 状态属性 | store 包含:nodes, edges, viewport (x, y, zoom), selected, userSelectionActive 等。 | const { zoom } = store.viewport; const selectedNodes = store.selected.nodes; | 这些是响应式引用,可直接用于计算或显示。 |
| 直接修改(危险) | 不推荐直接修改 store.nodes = [...],可能导致视图不更新或插件状态不一致。 | ❌ store.nodes = newNodes; | 应使用 setNodes, addEdges 等官方 API。 |
| 安全读取 | 适合用于状态监听、条件渲染、插件开发。 | watch(() => store.nodes.value, (newNodes) => { console.log('节点变更:', newNodes); }); | 结合 watch 实现状态变化的副作用。 |
| 低层操作 | 在特殊场景(如高性能批量更新)下,可结合 store 和 store.setState。 | store.setState((s) => ({ ...s, nodes: optimizedNodes })); | setState 是底层方法,需传入完整状态切片,风险高。 |
| 最佳实践 | 优先使用 useVueFlow 提供的 setNodes, setEdges 等函数。 | import { useVueFlow } from '@vue-flow/core'; const { setNodes, setEdges } = useVueFlow(); setNodes([...]); | 官方 API 会触发正确的更新流程和事件。 |
| 注意事项 | - | 1. useStore 主要用于读取和调试 2. 避免在业务逻辑中直接写入 store 3. 插件开发时可能需要 useStore 访问内部状态 | - |
8.2 插件扩展机制简介
| 概念 | 说明 | 代码示例 | 注意事项 |
|---|
| 插件系统 | Vue Flow 支持通过 plugins 属性扩展功能,如迷你地图、控制条、背景网格等。 | import { MiniMap, Controls, Background } from '@vue-flow/*'; <VueFlow :plugins="[minimap, controls, background]" /> | 插件是独立的 Vue 组件,注入到画布中。 |
| 常用官方插件 | - MiniMap: 小地图预览 - Controls: 缩放/重置控件 - Background: 网格背景 - NodeToolbar: 节点悬停工具栏 | const minimap = h(MiniMap, { nodeStrokeWidth: 3 }); const controls = h(Controls); const background = h(Background, { variant: 'dots' }); | 使用 h() (createElement) 创建 VNode。 |
| 插件配置 | 大多数插件支持 props 进行定制。 | h(Controls, { showZoom: true, showFitView: true, showInteractive: false }) | 参考各插件文档配置外观和行为。 |
| 自定义插件 | 可开发自己的插件组件,接收 id, vueFlowInstance 等 props。 | | |
<!-- MyPlugin.vue -->
<script setup>
defineProps(['id', 'vueFlowInstance'])
</script>
<template>
<div class="absolute top-4 left-4">自定义控件</div>
</template>
| 插件通信 | 插件可通过 vueFlowInstance 访问 useVueFlow 的所有函数。 | const { fitView, zoomIn } = vueFlowInstance; // 在插件内调用 | 实现与主画布的交互。 |
| 注册方式 | 在 <VueFlow> 的 :plugins 数组中传递插件 VNode。 | :plugins="[h(MiniMap), h(MyPlugin)]" | 顺序可能影响 z-index(后渲染的在上层)。 |
| 注意事项 | - | 1. 插件需从 @vue-flow/* 包导入 2. 确保插件组件轻量,避免性能问题 3. 自定义插件需处理响应式和生命周期 | - |
8.3 状态持久化:save 与 restore
| 方法 | 说明 | 代码示例 | 注意事项 |
|---|
| toObject | 将当前画布的节点、边、视口等状态序列化为普通对象。 | import { useVueFlow } from '@vue-flow/core'; const { toObject } = useVueFlow(); const flowData = toObject(); | 返回的数据是纯 JSON 友好的,适合存储。 |
| fromObject | 将保存的对象状态恢复到画布。 | const saved = localStorage.getItem('my-flow'); if (saved) { fromObject(JSON.parse(saved)); } | 会完全替换当前画布内容。 |
| 保存时机 | 用户点击”保存”按钮,或在 onPaneClick 等事件后自动保存(建议防抖)。 | const saveFlow = () => { const data = toObject(); localStorage.setItem('flow-data', JSON.stringify(data)); }; | 建议添加防抖,避免频繁写入。 |
| 恢复时机 | 组件 onMounted 时尝试从存储加载。 | onMounted(() => { const saved = localStorage.getItem('flow-data'); if (saved) { fromObject(JSON.parse(saved)); } }); | 确保 fromObject 在 Vue Flow 初始化后调用。 |
| 数据结构 | toObject 输出包含 nodes, edges, viewport,可直接 JSON.stringify。 | | |
{
"nodes": [{ "id": "n1", "type": "default", "position": { "x": 100, "y": 200 }, "data": {} }],
"edges": [{ "id": "e1", "source": "n1", "target": "n2" }],
"viewport": { "x": 0, "y": 0, "zoom": 1 }
}
| 自定义字段 | 可在 nodes[i].data 中存储业务数据,toObject 会一并保存。 | { data: { label: '任务1', priority: 'high' } } | 恢复后自定义节点可直接读取 data。 |
| 注意事项 | - | 1. fromObject 会清除现有内容,如有未保存更改需提示用户 2. 敏感数据注意不要存入 localStorage 3. 大型图考虑使用 IndexedDB | - |
8.4 图的撤销重做:useUndoRedo
| 概念 | 说明 | 代码示例 | 注意事项 |
|---|
| useUndoRedo | Vue Flow 官方插件,提供撤销(Undo)和重做(Redo)功能。 | import { useUndoRedo } from '@vue-flow/additional-components'; const { undo, redo, canUndo, canRedo } = useUndoRedo(); | 需从 @vue-flow/additional-components 导入。 |
| 启用方式 | 必须在 useVueFlow 之后调用。 | const { useVueFlow } = useVueFlow({ snapToGrid: true }); const { undo, redo } = useUndoRedo(); | 确保在正确的时机初始化。 |
| 核心函数 | - undo(): 撤销上一步操作 - redo(): 重做被撤销的操作 | const onUndo = () => { if (canUndo.value) undo(); }; const onRedo = () => { if (canRedo.value) redo(); }; | 调用前检查 canUndo/canRedo 避免无效操作。 |
| 响应式状态 | canUndo, canRedo 是 ref<boolean>,可用于禁用按钮。 | | |
<button :disabled="!canUndo" @click="undo">撤销</button>
<button :disabled="!canRedo" @click="redo">重做</button>
| 监听变更 | 可 watch canUndo/canRedo 响应状态变化。 | watch(canUndo, (can) => console.log('可撤销:', can)); | 用于日志或 UI 更新。 |
| 支持的操作 | 通常支持:节点增删/移动、边增删、连接/断开等。 | - | 具体支持范围取决于插件实现和 Vue Flow 版本。 |
| 与持久化结合 | 撤销栈不自动持久化,关闭页面即丢失。 | - | 如需持久化撤销历史,需自行实现复杂的状态快照管理。 |
| 注意事项 | - | 1. useUndoRedo 是插件,需正确安装和导入 2. 某些自定义操作(如直接修改 store)可能不被记录 3. 大型图注意撤销栈的内存占用 | - |
第九章:性能优化与大型图处理
当节点和边的数量增长到数百甚至上千时,图的渲染和交互性能会显著下降。本章介绍针对大型图的优化策略。
9.1 虚拟滚动与节点懒加载
| 概念 | 说明 | 实现方式 | 注意事项 |
|---|
| 问题 | 渲染大量节点(如 > 500)会导致页面卡顿、内存占用高。 | - | 直接渲染所有节点是性能瓶颈的根源。 |
| 虚拟滚动 | 仅渲染视口(可视区域)内的节点和边,视口外的不渲染或使用占位符。 | Vue Flow 原生支持通过 viewport 监听和 transform 计算,可结合 Intersection Observer 手动实现。 | 1. 需为每个节点 DOM 添加 data-node-id 等标识 2. 隐藏节点可 v-if 或 display: none 3. 官方未来可能提供更完善的虚拟滚动插件 |
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const nodeId = entry.target.dataset.nodeId;
if (entry.isIntersecting) {
showNode(nodeId);
} else {
hideNode(nodeId);
}
});
});
| 节点懒加载 | 初始只加载核心节点,用户交互时动态加载关联节点。 |
// 初始只加载根节点
setNodes(rootNodes);
// 当用户点击"加载更多"节点时
onNodeClick((node) => {
if (node.data.hasMore) {
const newNodes = fetchRelatedNodes(node.id);
setNodes([...nodes.value, ...newNodes]);
addEdges(fetchRelatedEdges(node.id));
}
});
| 结合使用 | 虚拟滚动解决”渲染多”,懒加载解决”数据多”。 | 先懒加载部分图,再对已加载的部分启用虚拟滚动。 | 架构复杂,需权衡开发成本与性能收益。 |
| 注意事项 | - | 1. 虚拟滚动需精确计算节点边界和视口 2. 懒加载需处理加载状态和错误 3. 两种技术都会增加代码复杂度 | - |
9.2 使用 noDragClassName 与 noPanClassName 优化交互
| 属性 | 说明 | 代码示例 | 注意事项 |
|---|
| noDragClassName | 设置一个 CSS 类名,当用户在此类名的 DOM 元素上按下鼠标时,禁止节点拖拽。 | | |
<template>
<div class="custom-node">
<div class="node-header">标题</div>
<div class="node-content">内容</div>
<button class="node-button no-drag">操作按钮</button>
</div>
</template>
// 在 useVueFlow 配置中设置
const vueFlowOptions = { noDragClassName: 'no-drag' };
.no-drag {
cursor: pointer; /* 此区域点击不会拖动节点 */
}
| noPanClassName | 设置一个 CSS 类名,当用户在此类名的 DOM 元素上按下鼠标时,禁止画布平移。 | const vueFlowOptions = { noPanClassName: 'no-pan' }; | 1. 适用于节点内需要拖拽选择或绘制的区域 2. 与 noDragClassName 类似,但针对的是画布平移 3. 可避免用户在节点内部操作时整个画布乱移动 |
| 性能收益 | 减少不必要的事件监听和坐标计算。 | - | 虽然单次操作开销小,但在高频交互下能显著提升流畅度。 |
| 注意事项 | - | 1. 确保类名唯一且不与其他样式冲突 2. 可组合使用:<div class="no-drag no-pan"> 3. 测试时验证交互是否按预期工作 | - |
9.3 减少重渲染:nodesDraggable、nodesConnectable
| 属性 | 说明 | 代码示例 | 注意事项 |
|---|
| nodesDraggable | 控制所有节点是否可拖拽。设为 false 可大幅提升性能,尤其在移动端。 | <VueFlow :nodesDraggable="false" /> 或 const { setNodesDraggable } = useVueFlow(); setNodesDraggable(false); | 1. 禁用后用户无法移动节点,适合只读场景 2. 大型图中拖拽事件监听和计算是性能消耗大户 3. 可通过 UI 按钮动态切换 |
| nodesConnectable | 控制所有节点是否可连接。设为 false 可禁用连接桩的连接功能。 | <VueFlow :nodesConnectable="false" /> | 1. 禁用后 Handle 仍可见但无法拖出连接线 2. 适合展示阶段,防止用户误操作 3. 与 nodesDraggable 结合实现完全静态的图展示 |
| edgesFocusable / nodesFocusable | 控制是否可聚焦(影响键盘导航和可访问性)。 | <VueFlow :edgesFocusable="false" :nodesFocusable="false" /> | 1. 如果不需要键盘操作,建议关闭 2. 进一步减少 DOM 事件和监听器 |
| disableKeyboard | 禁用所有内置键盘快捷键(如删除节点、撤销等)。 | <VueFlow :disableKeyboard="true" /> | 1. 减少全局事件监听 2. 避免与应用其他快捷键冲突 |
| 按需启用 | 根据用户操作动态开启功能。 | <VueFlow :nodesDraggable="isEditing" :nodesConnectable="isEditing" /> | 实现”查看”和”编辑”模式分离,优化只读性能。 |
| 注意事项 | - | 1. 这些属性是全局开关,影响所有节点/边 2. 单个节点的可拖拽性由 node.draggable 控制,优先级高于全局设置 3. 关闭不必要的功能是优化大型图最简单有效的方法 | - |
9.4 内存管理与事件销毁
| 问题 | 说明 | 解决方案 | 注意事项 |
|---|
| 内存泄漏 | 组件卸载后,事件监听器、定时器、观察者未被清除,导致内存无法释放。 | 使用 onUnmounted 清理:确保在组件销毁时清理自定义逻辑。避免全局变量引用:不要将节点或边的引用存储在全局对象中。检查第三方库:如使用 d3, dagre,确保其对象被正确释放。 | |
import { onUnmounted } from 'vue';
onUnmounted(() => {
// 清理自定义定时器
if (myInterval) clearInterval(myInterval);
// 清理自定义观察者
if (observer) observer.disconnect();
// Vue Flow 实例通常由组件管理,无需手动销毁
});
| 事件监听器堆积 | 频繁添加/删除节点或重复初始化 useVueFlow 可能导致事件监听器重复绑定。 | 确保 useVueFlow 在组件 setup 中只调用一次。使用 watch 时保存返回的 stop 函数并在 onUnmounted 中调用。 |
const stopWatch = watch(someSource, callback);
onUnmounted(() => {
stopWatch(); // 清理 watch
});
| 大型对象缓存 | 缓存未压缩的节点数据、历史快照等占用大量内存。 | 限制撤销栈大小:如果使用 useUndoRedo,可考虑限制历史步数。及时清理缓存:不再需要的数据及时设为 null。使用弱引用:对于缓存,可考虑 WeakMap/WeakSet。 |
// 限制撤销步数(如果插件支持)
const { undo, redo } = useUndoRedo({ maxHistory: 50 });
| 资源密集型操作 | 复杂的节点渲染(如 SVG 滤镜、大量文本)、频繁的 fitView 调用。 | 简化节点 UI:避免在节点内使用复杂组件或动画。防抖(Debounce):对 fitView、布局计算等操作添加防抖。 |
import { debounce } from 'lodash-es';
const debouncedFitView = debounce(() => fitView(), 300);
| 使用 DevTools 监控 | 通过浏览器 DevTools 的 Memory 和 Performance 面板分析内存和性能。 | 拍摄堆快照(Heap Snapshot)查找内存泄漏。录制性能分析(Performance Recording)找出卡顿原因。 | 定期进行性能测试,尤其是在功能迭代后。 |
| 注意事项 | - | 1. 组件卸载是关键:确保父组件正确销毁,触发 onUnmounted 2. 避免长生命周期引用:如将 node 对象存入 Vuex/Pinia 且不清理 3. 测试大图场景:用真实数据量进行压力测试 | - |
第十章:实战项目:构建可拖拽工作流编辑器
本章将综合运用前九章知识,从零构建一个功能完整的可拖拽工作流编辑器。
10.1 需求分析与功能拆解
| 核心需求 | 详细功能点 | 技术实现 |
|---|
| 可视化编辑 | - 画布支持缩放、平移 - 节点自由拖拽、连接 - 边的创建与删除 | 使用 Vue Flow 的 VueFlow 组件,启用 nodesDraggable, edgesConnectable。 |
| 节点库 (Palette) | - 左侧固定区域展示可拖拽的节点类型(如”开始”、“任务”、“条件”、“结束”) - 支持搜索和分类 | 使用 draggable 库或原生 dragstart 事件实现节点类型从侧边栏拖入画布。 |
| 节点配置 | - 双击节点或点击”配置”按钮弹出表单 - 可编辑节点名称、参数、条件等 | 使用 Modal 组件,表单数据绑定到节点的 data 字段。 |
| 连接逻辑 | - 支持有向边连接节点 - “条件”节点可分叉出多条边(带标签) - 边可删除 | 自定义 Edge 组件支持标签;通过 edges 数组管理连接关系。 |
| 布局与视图 | - 提供”自动布局”按钮(层级布局) - “适应画布”(fitView)和”重置”按钮 | 集成 dagre 实现自动布局,调用 fitView 和 setTransform。 |
| 保存与加载 | - 本地保存/加载(localStorage) - 提交到后端 API | 使用 toObject / fromObject 进行序列化,通过 axios 调用 REST API。 |
| 撤销重做 | - 支持 Ctrl+Z / Ctrl+Y 撤销/重做 | 集成 @vue-flow/additional-components 的 useUndoRedo。 |
| 状态持久化 | - 刷新页面后恢复上次编辑状态 | 在 onMounted 时尝试从 localStorage 加载数据。 |
| 只读模式 | - 提供”预览”模式,禁用所有编辑操作 | 动态切换 nodesDraggable, nodesConnectable, edgesFocusable 为 false。 |
10.2 组件结构设计与状态划分
项目目录结构:
src/
├── components/
│ ├── FlowEditor.vue # 主编辑器组件,包含画布和工具栏
│ ├── NodePalette.vue # 节点库侧边栏
│ ├── NodeConfigModal.vue # 节点配置弹窗
│ ├── MiniMap.vue # 小地图(可选)
│ └── Controls.vue # 自定义控制条
├── nodes/
│ ├── StartNode.vue # 自定义"开始"节点
│ ├── TaskNode.vue # 自定义"任务"节点
│ ├── ConditionNode.vue # 自定义"条件"节点
│ └── EndNode.vue # 自定义"结束"节点
├── hooks/
│ ├── useWorkflowSave.js # 保存/加载逻辑
│ └── useDagreLayout.js # 自动布局 Hook
├── stores/
│ └── workflowStore.js # (可选)Pinia 状态管理
└── api/
└── workflowApi.js # 后端交互 API
状态划分:
| 状态来源 | 状态内容 | 存储位置 | 说明 |
|---|
| Vue Flow 内部 | nodes, edges, viewport | useVueFlow 返回的响应式对象 | 核心图数据,由 VueFlow 组件自动管理。 |
| 应用状态 | isEditing, selectedNode, showConfigModal | FlowEditor.vue 的 ref 或 reactive | 控制 UI 交互状态。 |
| 节点数据 | 节点的业务参数(如任务名称、条件表达式) | 存储在 node.data 中 | toObject 会自动序列化 data 字段。 |
| 持久化状态 | 完整的流程图数据(nodes, edges, viewport) | localStorage 或后端数据库 | 用于保存和恢复工作流。 |
10.3 实现节点库与拖拽入画布
1. 节点库(NodePalette.vue)实现拖拽源:
<!-- NodePalette.vue -->
<template>
<div class="palette">
<div
v-for="type in nodeTypes"
:key="type.id"
class="palette-item"
draggable="true"
@dragstart="onDragStart($event, type)"
>
<component :is="type.component" :data="{ label: type.label }" />
</div>
</div>
</template>
// NodePalette.vue <script setup>
import { h } from 'vue';
const nodeTypes = [
{ id: 'start', label: '开始', component: 'StartNode' },
{ id: 'task', label: '任务', component: 'TaskNode' },
{ id: 'condition', label: '条件', component: 'ConditionNode' },
{ id: 'end', label: '结束', component: 'EndNode' },
];
const onDragStart = (event, nodeType) => {
// 将节点类型信息附加到 drag event
event.dataTransfer.setData('application/vue-flow', JSON.stringify(nodeType));
event.dataTransfer.effectAllowed = 'copy';
};
2. 画布(FlowEditor.vue)实现拖拽目标:
<!-- FlowEditor.vue -->
<template>
<div class="editor-container">
<!-- 左侧节点库 -->
<NodePalette />
<!-- 主画布 -->
<VueFlow
ref="vueFlow"
class="canvas"
:nodes="nodes"
:edges="edges"
:node-types="nodeTypes"
:edges-updatable="true"
:nodes-connectable="true"
@pane-click="onPaneClick"
@node-click="onNodeClick"
@dragover="onDragOver"
@drop="onDrop"
>
<!-- 插件 -->
<MiniMap />
<Controls />
</VueFlow>
<!-- 节点配置弹窗 -->
<NodeConfigModal
v-if="showConfigModal"
:node="selectedNode"
@close="showConfigModal = false"
@save="updateNodeData"
/>
</div>
</template>
// FlowEditor.vue <script setup>
import { ref, onMounted } from 'vue';
import { VueFlow, useVueFlow } from '@vue-flow/core';
import { useUndoRedo } from '@vue-flow/additional-components';
import NodePalette from './components/NodePalette.vue';
import NodeConfigModal from './components/NodeConfigModal.vue';
import StartNode from '../nodes/StartNode.vue';
import TaskNode from '../nodes/TaskNode.vue';
// ... 其他节点
// Vue Flow 状态
const {
setNodes,
setEdges,
addNodes,
addEdges,
toObject,
fromObject,
fitView
} = useVueFlow();
// 应用状态
const nodes = ref([]);
const edges = ref([]);
const selectedNode = ref(null);
const showConfigModal = ref(false);
// 定义节点类型
const nodeTypes = {
start: StartNode,
task: TaskNode,
// ... 映射
};
// 处理拖拽悬停
const onDragOver = (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
};
// 处理节点从侧边栏拖入
const onDrop = (event) => {
event.preventDefault();
const type = event.dataTransfer.getData('application/vue-flow');
if (!type) return;
const nodeType = JSON.parse(type);
// 将页面坐标转换为画布坐标
const position = vueFlow.project({ x: event.clientX, y: event.clientY });
const newNode = {
id: `n_${Date.now()}`, // 生成唯一 ID
type: nodeType.id,
position,
data: {
label: nodeType.label,
config: {} // 存放具体配置项
},
};
addNodes([newNode]);
};
// 节点点击事件
const onNodeClick = (event, node) => {
selectedNode.value = node;
showConfigModal.value = true;
};
// 更新节点配置
const updateNodeData = (updatedData) => {
const node = nodes.value.find(n => n.id === selectedNode.value.id);
if (node) {
node.data = { ...node.data, ...updatedData };
}
showConfigModal.value = false;
};
// 生命周期:组件挂载后尝试恢复数据
onMounted(() => {
const saved = localStorage.getItem('workflow-data');
if (saved) {
fromObject(JSON.parse(saved));
// 延迟执行 fitView 确保渲染完成
setTimeout(() => fitView(), 100);
}
});
// 集成撤销重做
const { undo, redo, canUndo, canRedo } = useUndoRedo();
10.4 数据保存与后端交互
1. 本地保存与加载(useWorkflowSave.js):
// hooks/useWorkflowSave.js
import { useVueFlow } from '@vue-flow/core';
export const useWorkflowSave = () => {
const { toObject, fromObject } = useVueFlow();
const STORAGE_KEY = 'workflow-data';
// 保存到 localStorage
const saveToLocal = () => {
const data = toObject();
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
console.log('工作流已保存到本地');
};
// 从 localStorage 加载
const loadFromLocal = () => {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
fromObject(JSON.parse(saved));
console.log('已从本地恢复工作流');
}
};
// 清除本地数据
const clearLocal = () => {
localStorage.removeItem(STORAGE_KEY);
console.log('本地工作流数据已清除');
};
return {
saveToLocal,
loadFromLocal,
clearLocal
};
};
2. 后端 API 交互(api/workflowApi.js):
// api/workflowApi.js
import axios from 'axios';
const api = axios.create({
baseURL: '/api/workflows', // 假设后端 API 地址
});
export const workflowApi = {
// 保存工作流
async save(workflowData) {
try {
const response = await api.post('/', workflowData);
return response.data;
} catch (error) {
console.error('保存失败:', error);
throw error;
}
},
// 加载指定 ID 的工作流
async load(workflowId) {
try {
const response = await api.get(`/${workflowId}`);
return response.data;
} catch (error) {
console.error('加载失败:', error);
throw error;
}
},
// 获取工作流列表
async list() {
try {
const response = await api.get('/');
return response.data;
} catch (error) {
console.error('获取列表失败:', error);
throw error;
}
}
};
3. 在 FlowEditor.vue 中集成保存功能:
<!-- 在 FlowEditor.vue 的 template 中添加工具栏 -->
<template>
<!-- ... -->
<div class="toolbar">
<button @click="saveToLocal">本地保存</button>
<button @click="loadFromLocal">本地加载</button>
<button @click="submitToBackend">提交到服务器</button>
<button @click="fitView">适应画布</button>
<button @click="() => undo()" :disabled="!canUndo">撤销</button>
<button @click="() => redo()" :disabled="!canRedo">重做</button>
</div>
<!-- ... -->
</template>
// FlowEditor.vue <script setup> 补充
import { useWorkflowSave } from '../hooks/useWorkflowSave';
import { workflowApi } from '../api/workflowApi';
const { saveToLocal, loadFromLocal } = useWorkflowSave();
// 提交到后端
const submitToBackend = async () => {
const flowData = toObject();
try {
await workflowApi.save(flowData);
alert('提交成功!');
} catch (error) {
alert('提交失败:' + error.message);
}
};
4. 后端数据模型(示例 - Node.js/Express):
// server/models/Workflow.js
const mongoose = require('mongoose');
const workflowSchema = new mongoose.Schema({
name: String,
data: { // 存储 toObject() 返回的完整对象
nodes: Array,
edges: Array,
viewport: Object
},
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Workflow', workflowSchema);
注意事项:
- 数据校验: 后端接收数据时需进行安全校验,防止恶意代码注入。
- 版本控制: 可考虑为工作流添加版本号,支持历史版本回滚。
- 权限控制: 实际项目中需加入用户认证和权限管理。