简介:LogicFlow 是一套面向业务流程可视化的前端框架,适合需要在业务系统内快速搭建流程图编辑器的中高级前端开发者。它提供节点与连线交互、撤销重做、对齐线、键盘快捷键等配套能力,并支持自定义节点、插件扩展与 MVVM 式数据驱动,可满足审批流、拓扑图、编排画布等类流程图场景。资源包共 333 个文件,约 997KB,以 tsx 与 ts 源码为主,辅以 md 文档、js 脚本、png 示意图、css/less 样式及 json 配置,另含少量 html、vue 示例与字体图标文件,结构完整,便于对照源码理解框架实现。目前已有 3784 人学习下载。通过该资源可掌握核心 API 用法、自定义节点与插件机制、样式组织方式及工程化配置,快速将流程图能力落地到实际项目中。
1. 流程可视化前端框架 LogicFlow:从流程图到可交互业务编排
很多团队做流程可视化,第一反应是找一张流程图库把节点和连线画出来,结果真到业务里才发现:图能画,但拖拽后数据对不上、节点要自定义却卡在渲染层、连线要加条件分支就彻底失控。LogicFlow 这个前端框架解决的正是这类问题——它把「画图」和「图背后的数据模型」拆开,用一套可扩展的节点、边、插件机制,让你在浏览器里搭出能编辑、能校验、能回写业务状态的流程图。它适合谁?适合需要做审批流配置、任务编排、数据管道设计器、低代码画布的前端工程师。你不需要从零实现 SVG 命中检测和贝塞尔曲线,但你需要理解它的数据驱动思路,否则自定义节点时会一直踩坑。下面按「是什么 → 怎么跑通 → 怎么扩展 → 坑在哪 → 怎么进阶」的顺序讲透。
2. LogicFlow 的架构选型:为什么是数据驱动而不是 DOM 驱动
2.1 图形层与数据层分离的核心设计
LogicFlow 的底层渲染基于 SVG,但它对外暴露的是一份 JSON 图数据。你调用lf.render(graphData)时,框架内部把节点和边分别实例化,再挂到 SVG 画布上。这个设计的关键在于:你操作的是数据,不是 DOM。比如移动一个节点,框架先更新内部nodeModel的坐标,再触发视图重绘,最后通过事件把变更抛给你。这和直接操作 SVG 元素的库有本质区别——后者在节点数量上去之后,状态同步会变成玄学。
常见做法是:初始化时传入一份符合 LogicFlow 数据格式的图数据,后续所有交互都通过lf实例的 API 或事件来驱动。这样做的好处是撤销重做、导入导出、协同编辑都有统一的切入点。我一般会在项目里把lf.getGraphData()的返回值直接存到状态管理里,而不是去读 DOM 属性。
2.2 节点、边、插件三层扩展模型
LogicFlow 的扩展点分三层。第一层是节点类型,通过继承RectNode、CircleNode等基类,重写setHtml或setShape来定义外观。第二层是边类型,控制连线样式、箭头、标签位置。第三层是插件,比如菜单、选区、拖拽面板,这些不改变图数据本身,但增强交互。
选型理由很直接:如果你的业务只需要静态展示,用任何 SVG 库都行;但只要涉及「用户能改图,改完要存下来,存下来还要能还原」,LogicFlow 的模型层就省掉了你自己维护状态机的成本。参数上,节点基类的width、height、x、y是必设的,properties字段可以挂任意业务数据,这个字段在导出时会原样保留。
2.3 最小可运行示例:用 npm 在本地跑通一张可拖拽流程图
先建一个空目录,初始化后安装依赖。这里用 Vite 起一个最简环境,避免 Webpack 配置干扰。
npm create vite@latest lf-demo -- --template vanilla cd lf-demo npm install npm install @logicflow/core @logicflow/extension然后在main.js里写入初始化逻辑:
import LogicFlow from '@logicflow/core' import '@logicflow/core/dist/style/index.css' // 画布容器,宽高必须显式设置,否则 SVG 尺寸为 0 const container = document.getElementById('app') container.style.width = '100vw' container.style.height = '100vh' const lf = new LogicFlow({ container, grid: { size: 20, // 网格大小,影响吸附精度 visible: true, // 是否显示网格线 type: 'dot' // 网格样式,dot 比 line 更不抢视觉 }, keyboard: { enabled: true // 开启键盘删除、撤销快捷键 }, stopMoveGraph: false // 允许拖拽画布空白处平移 }) // 渲染初始图数据,nodes 和 edges 的 id 必须唯一 lf.render({ nodes: [ { id: 'node-1', type: 'rect', x: 200, y: 200, text: '开始', properties: { bizType: 'start' } }, { id: 'node-2', type: 'rect', x: 500, y: 200, text: '审批', properties: { bizType: 'approve' } } ], edges: [ { id: 'edge-1', type: 'polyline', sourceNodeId: 'node-1', targetNodeId: 'node-2', text: '提交' } ] }) // 监听节点点击,验证事件系统是否工作 lf.on('node:click', (args) => { console.log('点击节点', args.data.id, args.data.properties) })这段代码的逻辑说明:container必须是有实际尺寸的 DOM 元素,否则 SVG 渲染出来是空白,这是新手第一个翻车点。grid.size设为 20 意味着节点移动时会按 20 像素吸附,如果业务需要自由定位就设为 1 或关闭。properties是业务数据的挂载点,导出图数据时会完整保留,适合存节点对应的后端 ID 或配置项。lf.on的事件名是固定字符串,node:click的args.data就是该节点的数据快照,直接读properties即可。
跑起来后你应该能看到两个矩形节点和一条折线,拖拽节点时连线跟随,点击节点控制台输出业务属性。这一步通了,说明环境没问题,接下来才是真正的扩展。
3. 自定义节点与边:把业务语义画进流程图
3.1 继承基类重写 setHtml 的完整步骤
LogicFlow 内置的rect、circle、polygon只够做原型。真实业务里节点往往要显示图标、状态徽标、多行文本。自定义节点的标准做法是继承RectNode和RectNodeModel,分别控制视图和模型。
import { RectNode, RectNodeModel } from '@logicflow/core' // 模型层:定义节点尺寸、锚点、业务字段 class TaskNodeModel extends RectNodeModel { setAttributes() { this.width = 160 this.height = 60 // 锚点决定连线从哪出发,四个方向各一个 this.anchors = [ { x: 0, y: 0.5, id: 'left' }, { x: 1, y: 0.5, id: 'right' }, { x: 0.5, y: 0, id: 'top' }, { x: 0.5, y: 1, id: 'bottom' } ] } // 根据业务状态返回不同边框颜色 getNodeStyle() { const style = super.getNodeStyle() const status = this.properties.status if (status === 'error') style.stroke = '#f5222d' if (status === 'success') style.stroke = '#52c41a' return style } } // 视图层:用 SVG 或 HTML 渲染内容 class TaskNode extends RectNode { setHtml(rootEl) { const { properties } = this.props.model const el = document.createElement('div') el.className = 'task-node' el.innerHTML = ` <div class="task-title">${properties.label || '未命名'}</div> <div class="task-status">${properties.status || 'pending'}</div> ` rootEl.innerHTML = '' rootEl.appendChild(el) } } // 注册后 type 为 'task' 的节点就会用这套渲染 lf.register('task', () => ({ view: TaskNode, model: TaskNodeModel }))逻辑说明:setAttributes在节点实例化时调用一次,适合设固定尺寸和锚点。getNodeStyle每次重绘都会调用,适合根据properties动态改样式。setHtml里操作的是rootEl,这是框架给每个节点分配的 HTML 容器,直接覆盖innerHTML即可。参数上,anchors的x、y是相对节点宽高的比例值,0 到 1 之间,id用于连线时指定锚点。
3.2 边类型定制:折线、曲线与条件分支标签
边的自定义和节点类似,继承PolylineEdge和PolylineEdgeModel。条件分支场景下,边需要显示标签,标签位置和内容由模型控制。
import { PolylineEdge, PolylineEdgeModel } from '@logicflow/core' class ConditionEdgeModel extends PolylineEdgeModel { setAttributes() { // 标签位置:0 起点,1 终点,0.5 中点 this.textPosition = 0.5 // 允许用户双击编辑标签 this.editable = true } getEdgeStyle() { const style = super.getEdgeStyle() // 条件为真时用绿色虚线 if (this.properties.condition === 'true') { style.stroke = '#52c41a' style.strokeDasharray = '5 5' } return style } } class ConditionEdge extends PolylineEdge {} lf.register('condition', () => ({ view: ConditionEdge, model: ConditionEdgeModel }))这里的关键参数是textPosition,它决定标签沿路径的位置比例。editable开启后,双击边标签会进入编辑态,编辑完成触发edge:update事件,你在事件里把新标签写回properties。注意:边的properties和节点一样会随图数据导出,所以条件表达式、优先级这类信息都放这里,不要塞到text里,否则解析时还要拆字符串。
3.3 用插件扩展交互:菜单、选区与拖拽面板
LogicFlow 的@logicflow/extension包提供了常用插件。以菜单插件为例,右键节点弹出操作项:
import { Menu } from '@logicflow/extension' import '@logicflow/extension/lib/style/index.css' lf.use(Menu) lf.extension.menu.setMenuConfig({ nodeMenu: [ { text: '删除节点', callback(node) { lf.deleteNode(node.id) } }, { text: '复制节点', callback(node) { const data = lf.getNodeModelById(node.id).getData() data.id = `${data.id}-copy` data.x += 40 data.y += 40 lf.addNode(data) } } ], edgeMenu: [ { text: '删除连线', callback(edge) { lf.deleteEdge(edge.id) } } ] })逻辑说明:lf.use(Menu)注册插件后,lf.extension.menu才存在。setMenuConfig接收nodeMenu和edgeMenu两个数组,每项的callback参数是当前节点或边的数据对象。复制节点时直接改id和坐标再addNode,框架会自动处理渲染。参数上,菜单项的text是显示文案,没有图标配置项,需要图标得自己改 CSS 背景。
4. 数据流转与事件体系:让流程图驱动业务状态
4.1 图数据的导出、导入与版本兼容
lf.getGraphData()返回的 JSON 包含nodes和edges两个数组,每个节点有id、type、x、y、text、properties,每条边有id、type、sourceNodeId、targetNodeId、properties。这份数据可以直接JSON.stringify存后端。
导入时用lf.render(data)全量替换,或者用lf.addNode、lf.addEdge增量添加。全量替换会清空当前画布和撤销栈,增量添加则保留历史。我一般会在保存前调一次lf.getGraphData(),然后做一层校验:检查是否有孤立节点、是否有环、properties里的必填字段是否齐全。校验不通过就不让保存,避免脏数据进库。
版本兼容是个容易被忽略的点。如果节点类型改名了,旧数据里的type就找不到对应注册项,渲染会直接报错。常见做法是在导入前做一次数据迁移,把旧type映射成新type,或者在lf.register时同时注册旧名称作为别名。
4.2 监听节点与边事件做实时校验
LogicFlow 的事件名遵循元素:动作格式,常用的有node:click、node:dnd-drag、edge:add、edge:delete、graph:updated。实时校验的典型场景是:用户连线时判断是否形成环。
lf.on('edge:add', (args) => { const { sourceNodeId, targetNodeId } = args.data // 简单环检测:从目标节点出发能否回到源节点 const graphData = lf.getGraphData() const adjacency = {} graphData.edges.forEach((e) => { if (!adjacency[e.sourceNodeId]) adjacency[e.sourceNodeId] = [] adjacency[e.sourceNodeId].push(e.targetNodeId) }) const visited = new Set() const stack = [targetNodeId] while (stack.length) { const cur = stack.pop() if (cur === sourceNodeId) { lf.deleteEdge(args.data.id) alert('不允许形成环') return } if (visited.has(cur)) continue visited.add(cur) ;(adjacency[cur] || []).forEach((n) => stack.push(n)) } })逻辑说明:edge:add在边被添加到数据模型后触发,此时args.data已包含完整边信息。环检测用深度优先遍历,从目标节点出发看能否回到源节点。如果检测到环,立即deleteEdge并提示。参数上,adjacency是临时的邻接表,每次事件都重新构建,节点规模在几百以内性能没问题,上千节点建议改成增量维护。
4.3 与后端联调:保存格式与回显一致性
前后端联调最容易出问题的是坐标和尺寸。LogicFlow 的x、y是节点中心点坐标,width、height是节点尺寸。如果后端存储时只存了左上角坐标,回显时节点位置就会偏移。约定好:存中心点,存完整 properties,存 type 名称。
回显时还有一个坑:如果后端返回的edges里sourceNodeId或targetNodeId指向不存在的节点,render会静默丢弃这条边,不报错。所以联调阶段要在render后检查lf.getGraphData().edges.length是否和传入的一致,不一致就说明有边被丢了,去查节点 ID 是否匹配。
5. 避坑与排查:自定义节点不显示、连线错位、性能骤降
5.1 自定义节点注册后画布空白
现象:lf.register调用了,render也传了对应type,但画布上什么都没有,控制台无报错。
原因:最常见的是setHtml里rootEl被覆盖后没有正确设置尺寸,或者setAttributes里width、height设成了 0。另一个原因是注册时机在render之后,框架找不到类型定义。
解决:确保lf.register在lf.render之前调用。在setAttributes里打印this.width确认非零。setHtml里如果用了 HTML 内容,给根元素设width: 100%; height: 100%,否则内容可能溢出到不可见区域。
5.2 连线锚点错位或连线穿节点
现象:从节点拖出的连线起点不在鼠标位置,或者连线直接穿过节点内部。
原因:anchors的坐标是相对节点宽高的比例,但如果你在setAttributes里改了width却没同步改anchors,锚点位置就会偏。另外,PolylineEdge默认走最短路径,节点密集时会穿节点。
解决:锚点坐标始终用 0 到 1 的比例值,不要写像素。穿节点问题可以换用BezierEdge或自定义边的getPath方法,让路径绕开节点包围盒。参数上,anchors的id在连线时用于指定具体锚点,不指定则自动选最近的。
5.3 节点数量过百后拖拽卡顿
现象:几十个节点时流畅,超过一百个后拖拽明显掉帧,撤销重做要等一两秒。
原因:每次拖拽都触发全量重绘,且getNodeStyle、getEdgeStyle里如果有复杂计算,会被高频调用。另外,setHtml里如果创建了大量 DOM,内存和渲染压力都会上去。
解决:把getNodeStyle里的计算缓存起来,只在properties变化时更新。节点超过两百个时,考虑用lf.setTheme统一主题,减少逐个节点的样式计算。如果业务允许,开启grid吸附减少重绘次数。实测把setHtml换成纯 SVG 的setShape能再降一档开销,但开发成本高,按需选择。
5.4 撤销重做后业务数据丢失
现象:Ctrl+Z 撤销后,节点位置回去了,但properties里的业务字段变回了旧值,或者直接丢了。
原因:LogicFlow 的撤销栈记录的是图数据快照,如果你在properties里存了非序列化的对象(比如函数、DOM 引用),快照会失真。另外,通过lf.setProperties改属性如果没走框架 API,撤销栈不会记录。
解决:properties里只放可 JSON 序列化的值。改属性统一用lf.setProperties(nodeId, key, value),不要直接改nodeModel.properties。如果确实需要存复杂结构,存 ID,运行时用 ID 去外部映射表取。
5.5 导入旧数据时节点类型找不到
现象:render旧版本保存的数据,控制台报Unknown node type,画布只渲染出一部分。
原因:节点类型注册名改了,或者插件没加载导致扩展节点类型缺失。
解决:在render前做一次数据清洗,把旧type映射成新type。如果用了插件提供的节点类型(比如Menu不提供节点,但DndPanel提供),确保lf.use在render之前执行。稳妥做法是写一个migrateGraphData函数,按版本号逐级升级数据格式。
6. 进阶技巧:用 LogicFlow 做可配置审批流的三个关键点
第一个关键点是节点模板化。审批流里节点类型固定,但每个节点的表单字段不同。我的做法是把节点properties设计成{ templateId, formData },templateId对应后端配置的模板,formData存用户填的值。渲染时根据templateId决定节点内显示哪些字段。这样新增审批类型不用改前端代码,只加模板配置。
第二个关键点是边上的条件表达式。审批流的分支条件不能只存文本,要存可执行的结构。我一般存{ field, operator, value }这样的对象,前端做校验时解析成布尔值,后端执行时用同一套规则。LogicFlow 的边properties支持嵌套对象,直接放进去即可,导出时不会丢。
第三个关键点是版本快照与回滚。审批流配置改错了要能回滚。利用lf.getGraphData()在每次保存时生成一份快照,存到版本表里。回滚时把快照render回去。注意快照要包含properties全量,否则回滚后业务数据对不上。
验证方法很简单:搭一个最小审批流,两个审批节点加一个条件分支,保存后刷新页面看回显是否一致,再改条件分支的表达式看校验是否生效。如果这两步都通过,说明数据流转没问题,可以往生产环境推。
我自己的习惯是:每次改自定义节点或边之后,先跑一遍「新增节点 → 连线 → 改属性 → 导出 → 清空 → 导入」的完整链路,确认数据无损再继续写业务逻辑。这个习惯帮我省掉了至少三次上线前的紧急回滚。希望帮到你。
本文还有配套的精品资源,点击获取