
简介LogicFlow 是一套面向前端开发者的流程可视化框架可快速在业务系统中嵌入类流程图编辑与交互能力适合需要构建审批流、编排设计器等场景的中高级前端工程师。压缩包内为完整工程资源共333个文件以 TypeScript/TSX 源码为主另含 JavaScript 脚本、Markdown 说明文档、CSS 样式、PNG 图标等覆盖核心逻辑、示例工程与样式定义整体仅约997KB轻量便于快速查阅。目前已有3784人学习下载。借助该包可了解 LogicFlow 的安装引入、数据模型结构以及自定义节点、边、插件扩展、撤销重做、对齐参考线、快捷键等配套机制能帮助开发者少走弯路快速上手并拓展到个性化业务流程可视化。1. 谁说前端画流程图只能对着几百行SVG硬啃时间回到三个月前产品丢过来一个在线审批流编辑器的原型图要求一周内跑通能拖、能连、能保存的版本。我选了LogicFlow一个基于svg渲染的flowchart/graph-editor前端框架核心代码用TypeScript写成底层按MVVM的思路管理整张图的model与view。它解决的痛点是市面上很多流程图库要么只能出图、拿不到结构化数据要么自带一套重型工作流引擎根本没法轻量嵌入业务系统。LogicFlow把“图编辑”这件事单独拆出来让开发者自己掌控业务数据。它适合做低代码流程设计器、规则编排、节点式配置后台的人。如果你们团队正好要做这种能被业务方反复改节点状态的编辑器它尤其对路。2. 整体架构与工程结构MVVM分层、最小实例与core/extension的边界2.1 一个svg图编辑器为什么需要MVVM如果你直接操作svg那一堆g、rect、path拖拽、增删节点、改样式这些功能写下来代码很快会变成没人敢碰的毛线团。LogicFlow把整张图拆成三层数据层由GraphModel、NodeModel、EdgeModel组成负责节点的位置、尺寸、文本、业务字段视图层根据model生成svg节点和边交互层把鼠标事件统一转发成业务事件比如node:click、edge:click。三层之间靠一张内部维护的diff表同步你在业务代码里改了一个nodeModel的properties视图会自动局部更新不需要你手动去改DOM属性。我一般把GraphModel当成整个编辑器的“黑匣子”入口它管着画布上所有节点和边的增删查改还管事件分发。NodeModel则是单个节点的数据内核宽高、坐标、文本、自定义业务字段全挂在上面。理解这一层后面写自定义节点才不会觉得API散得到处都是。这套设计最大的好处是把“图”变成了可序列化的数据而不是一堆不可恢复的DOM节点。2.2 初始化最小实例核心参数与两种渲染路径先把依赖装上core是核心包extension是官方扩展包后面做拖拽面板、右键菜单都要用到它。npm install logicflow/core npm install logicflow/extension初始化一个最小实例代码很短import LogicFlow from logicflow/core; import logicflow/core/dist/style/index.css; const lf new LogicFlow({ container: document.querySelector(#flow-container), grid: { size: 10, visible: true, type: dot, }, edgeType: polyline, snapline: true, isSilent: false, });这里几个参数值得多说一句。container是画布挂载点必须是一个已经存在于DOM里的元素。grid开了之后才有网格吸附效果size控制网格间距type换成dot就是点状网格视觉上比实线网格清爽。edgeType是默认连线类型polyline折线最常用适合审批流bezier是曲线适合偏自由拓扑的场景。snapline是吸附对齐线拖节点时出现十字参考线业务方拖起来会顺手很多。isSilent是只读模式打开之后整个画布只能看不能编辑适合做预览页。数据渲染走render方法传入的就是一张“图数据”lf.render({ nodes: [ { id: start_1, type: rect, x: 120, y: 100, text: 流程开始, properties: { status: running }, }, ], edges: [], });render把JSON数据变成图。id必须全局唯一type必须对应已注册的节点类型rect是内置矩形节点。x和y是svg画布坐标注意是节点的中心点坐标不是左上角。text可以直接传字符串也可以传一个带value和x、y的对象来做更精细的文本定位。properties是留给业务方的扩展字段状态、审批人、表单配置都往这里塞。注意render里如果出现重复id后注册的那个节点会被静默忽略不会报错排查时很容易漏掉。除了render还有一个renderModel路径。它接收的是已经构建好的Model实例数组适合服务端渲染或首屏性能优化的场景绕过了内部从JSON构建Model的过程。日常开发先用render就够了等真遇到大数据量卡顿时再考虑走renderModel。2.3 工程目录速览core、extension、engine的边界感装完包之后你会在node_modules里看到logicflow下面几个子包它们的边界很清晰logicflow ├── core # 核心包图模型、节点/边渲染、事件、注册表 ├── extension # 扩展包Menu、Control、DndPanel、MiniMap 等插件 ├── engine # 执行引擎把图数据跑成可流转的状态机 └── examples # 示例工程自定义节点、插件组合的democore是框架本体只管图编辑这件事不掺业务。extension是官方按可插拔思路沉淀的现成插件像右键菜单、控制栏、拖拽面板都在里面。engine是给需要真正执行流程的场景用的比如审批流要走节点流转、触发事件就靠它跑起来。我们做编辑器90%时间是在core和extension里折腾engine属于进阶选项先了解存在即可。2.4 数据出口getGraphData与回显闭环图编辑器的核心价值在于“能保存、能还原”LogicFlow的数据出口很干脆const graphData lf.getGraphData(); console.log(graphData); // nodes: [{ id, type, x, y, text, properties }] // edges: [{ id, sourceNodeId, targetNodeId, points, text, properties }]保存的时候把这个JSON整体丢给后端下次进入页面再用lf.render(graphData)回显。这个闭环是选型时最有说服力的一点编辑过程的产物是结构化数据不是div坐标集合。我一般会在getGraphData外面再包一层业务协议带上schema版本号将来数据结构升级时还能做兼容迁移。3. 核心模块拆解图模型、节点注册与自定义节点的完整链路3.1 GraphModel与NodeModel先找到操作入口操作任何节点前先拿到它的模型对象。GraphModel是画布级入口提供addNode、deleteNode、getNodeById这些全局能力。NodeModel是节点级内核x、y、width、height、text、properties都在它身上。const graphModel lf.getGraphModel(); const nodeModel lf.getNodeModelById(node_1); console.log(nodeModel.properties); // 业务字段 console.log(nodeModel.x, nodeModel.y); // 中心点坐标nodeModel拿到手之后可以改属性、改样式、改业务字段。注意一点直接赋值nodeModel.x 200虽然能改坐标但不会触发视图刷新。要走它的set方法比如setProperties、setText改完数据后视图自动更新。这跟MVVM的响应式原理是一回事只不过这里没有用Proxy做自动依赖收集而是显式调用set方法。3.2 写一个自定义审批节点继承内置模型默认矩形满足不了“审批人状态颜色”这种需求时最简单的做法是继承RectNodeModel覆写数据初始化和样式逻辑import { LogicFlow, RectNode, RectNodeModel } from logicflow/core; class ApproveNodeModel extends RectNodeModel { initNodeData(data) { super.initNodeData(data); this.width 120; this.height 56; this.text.value 审批节点; } getNodeStyle() { const style super.getNodeStyle(); const { status } this.properties; if (status rejected) { style.stroke #ff4d4f; style.fill #fff1f0; } else if (status approved) { style.stroke #52c41a; style.fill #f6ffed; } return style; } } const approveNode { type: approve-node, model: ApproveNodeModel, view: RectNode, }; lf.register(approveNode);这段代码只在model层做文章view直接复用内置的RectNode。initNodeData在节点初始化时调用一次这里设置宽高和默认文本getNodeStyle在每次渲染前被调用返回的style对象会合并到svg元素的属性上。当properties里的status变化时节点边框和填充色自动跟着变这就是数据驱动视图最直观的体现。注册之后render数据里就可以直接使用这个类型lf.render({ nodes: [ { id: approve_1, type: approve-node, x: 200, y: 150, properties: { status: pending } }, ], edges: [], });3.3 完全自定义外观用h函数拼svg内置view满足不了特殊外观时再考虑写自定义view。LogicFlow提供了一个h函数用来生成svg虚拟节点写法跟前端常见的虚拟节点差不多import { h } from logicflow/core; class CustomApproveView extends RectNode { getShape() { const { model } this.props; return h(g, {}, [ h(rect, { x: -model.width / 2, y: -model.height / 2, width: model.width, height: model.height, rx: 6, fill: #ffffff, stroke: model.properties.status rejected ? #ff4d4f : #333333, }), h(text, { x: 0, y: 0, textAnchor: middle, dominantBaseline: central, }, model.text.value), ]); } }getShape里返回的h函数嵌套结构最终会渲染成svg的g、rect、text标签。props里能拿到model和graphModel所以视图完全由数据驱动。注意坐标从负数开始画因为model.x和model.y是节点中心点svg内部坐标以中心为原点。注册时view换成CustomApproveView即可。3.4 事件链路从点击到业务更新自定义节点做完接下来的需求通常是“点击节点改状态”。LogicFlow的事件在graphModel上冒泡到lf实例lf.on(node:click, ({ data }) { const nodeModel lf.getNodeModelById(data.id); if (nodeModel.properties.status pending) { nodeModel.setProperties({ status: approved }); } });node:click回调的data是节点数据快照直接改data不会生效必须拿model再调用set方法。常用事件还有node:drag拖拽结束、edge:click点击连线、blank:click点击空白处。业务层只用lf.on这一套接口不需要碰svg原生事件这层封装省了不少事。4. 插件体系与内置能力拖拽面板、右键菜单与控制栏接入实录4.1 插件从哪来怎么挂官方把常用能力都做成了插件引入即用import LogicFlow from logicflow/core; import { Menu, Control, DndPanel, Snapshot } from logicflow/extension; const lf new LogicFlow({ container, plugins: [Menu, Control, DndPanel, Snapshot], });plugins数组里的每一项都是扩展包导出的构造函数框架初始化时统一执行插件的install逻辑之后实例上会挂出对应的插件句柄。不同版本里句柄是挂在lf上还是lf.extension上略有差异拿到实例后打印一下console.log(lf)就能确认不用死记。如果团队里有多个编辑器场景要复用同一套公共事件逻辑我一般会写成自定义插件挂进来而不是在业务组件里到处复制import { BasePlugin } from logicflow/core; class LogPlugin extends BasePlugin { static pluginName LogPlugin; install() { this.lf.on(node:click, ({ data }) { console.log(plugin capture:, data.id); }); } destroy() { this.lf.off(node:click); } }插件可以访问lf实例和graphModel既能挂事件也能加工具栏天然适合做跨业务的公共逻辑收敛。4.2 DndPanel把自定义节点拖进画布画流程图第一步通常是“从左侧面板拖节点”。DndPanel插件安装后用setPatternItems配置面板项lf.dndPanel.setPatternItems([ { label: 审批节点, type: approve-node, icon: data:image/svgxml;base64,..., }, { label: 结束节点, type: rect, icon: data:image/svgxml;base64,..., }, ]);label是面板上显示的文字type必须是已经注册过的节点类型icon是一个svg或者图片的dataURL。DndPanel只负责拖拽和落点真正创建节点的还是注册表。如果你拖进去的节点全叠在左上角九成是grid没开或者落点坐标计算没换算容器偏移这个坑下面避坑章专门展开。4.3 Menu右键菜单在节点上挂业务操作Menu插件提供右键菜单能力可以针对节点、边、画布空白处分别配置lf.extension.menu.addMenuConfig({ nodeMenu: [ { text: 置为通过, callback(nodeModel) { nodeModel.setProperties({ status: approved }); }, }, { text: 删除节点, callback(nodeModel) { lf.deleteNode(nodeModel.id); }, }, ], graphMenu: [], edgeMenu: [], });nodeMenu回调的第一个参数是NodeModel直接改数据即可graphMenu是空白区域的菜单edgeMenu是右键连线的菜单。这套能力特别适合做“受控编辑”只暴露你想让用户点的那几个操作其他编辑能力收起来。4.4 Control与Snapshot控制栏和导出图片Control插件给画布右下角加上缩小、放大、自适应、重置的悬浮按钮Snapshot插件把当前svg导出成图片lf.extension.control.resetZoom(); lf.extension.snapshot.getSnapshot(process-preview.png);这两个能力在预览大流程图和做流程存档验证时很有用。导出图片实际是把svg转成canvas再toDataURL如果图里有跨域的外部图片资源或特殊字体转换会失败需要让资源同源或者提前转成base64这也是个容易被忽略的坑。5. 避坑指南与常见问题排查做流程编辑器时我踩过的五个坑5.1 现象一节点拖进画布后叠在左上角现象从DndPanel拖节点进画布位置永远定在0,0附近怎么拖都过不去一度以为是玄学。原因最常见的是初始化时没开grid画布缺少吸附基准落点坐标算出来就是原点附近。另一种情况是自己在拖拽回调里手动设置了坐标但没把鼠标的clientX/clientY换算成画布坐标系offset没减算出来自然不对。解决初始化时把grid和snapline都开上让框架接管落点计算。非要自定义落点逻辑时统一用container.getBoundingClientRect()做一次坐标换算再把容器的偏移量减掉得到干净的画布坐标。5.2 现象二自定义节点外观正常锚点却全挤在中心现象继承RectNodeModel改了宽高之后连线的起点和终点全跑到节点中心线条直接穿过文字。原因RectNodeModel的锚点位置是按内置样式计算的你覆写getNodeStyle或改了width/height后锚点没有同步跟着新尺寸走默认值就兜底成了中心点。解决在自定义model里显式覆写getAnchor返回四个边缘中点的坐标class ApproveNodeModel extends RectNodeModel { getAnchor() { const { x, y, width, height } this; return [ { x, y: y - height / 2, id: top }, { x: x width / 2, y, id: right }, { x, y: y height / 2, id: bottom }, { x: x - width / 2, y, id: left }, ]; } }锚点稳定之后边上的sourceAnchorId和targetAnchorId保存回显才不会乱。5.3 现象三数据回显时边莫名丢失现象后端存好的graphData用lf.render()渲染出来节点全在边少了一半。原因每条edge必须同时指向nodes里真实存在的sourceNodeId和targetNodeId而且edge.type要和初始化时的edgeType或已注册的连线类型一致。很多后端在存储时过滤掉了一部分“看起来没关系的节点”导致边指向了不存在的节点框架直接丢弃了这条边。解决回显前加一层数据校验遍历edges把sourceNodeId或targetNodeId不在nodes集合里的边过滤掉或者打告警日志。我一般把这个校验放在数据适配层里做不指望前端框架兜底。5.4 现象四样式全乱了或控制台一堆css加载报错现象图能渲染出来但网格点、锚点、右键菜单面板的样式全不对控制台还飘着几个css资源加载失败。原因只引了JavaScript忘了引logicflow/core的样式文件。在打包工具里如果css被tree-shaking掉也会出现同样的症状。解决入口处把样式引进来import logicflow/core/dist/style/index.css;extension包里的插件样式也一样按需引入。打包之后别只在开发模式看一眼生产模式跑一遍再交测试不然容易在环境差异上翻车。5.5 现象五切换页面后旧图还在事件重复触发现象从编辑器页切到别的页再回来旧流程图没有消失点击节点事件一次触发两三次。原因组件卸载时只清掉了业务数据没有销毁LogicFlow实例。实例上绑定的DOM事件和graphModel事件还残留在旧容器上重新进入时又创建了新的实例新旧监听叠加在一起。解决组件卸载钩子里调用lf.destroy()释放实例绑定的监听和DOM。每次新建实例前先判断容器是否已经初始化过。这套框架的destroy方法就是后悔药忘了一次就知道疼了从那以后我再也没省过这一步。6. 二次开发改造思路用properties承接业务字段再做一层数据适配6.1 把业务字段塞进properties而不是text第一次写自定义节点时我图省事把审批人、状态、表单配置全塞进text里结果getGraphData导出的text长成一段JSON业务方看数据时一脸懵。后来统一换成properties存业务字段text只放给人看的一句话。建议每个自定义节点先定义好properties的类型结构type ApproveProperties { approver: string; status: pending | approved | rejected; formConfig: Recordstring, unknown; version: number; };这样TypeScript能在编译期兜住一半错误model里的getNodeStyle也只关心status其他业务字段不会污染视图逻辑。6.2 写一个adapter对接后端协议大多数情况下后端不会直接存LogicFlow的graphData它有自己的流程定义格式。前端不可能让后端迁就自己所以数据进出两侧都要补适配函数function toServer(graphData) { return { processId: graphData.id, nodes: graphData.nodes.map(n ({ nodeId: n.id, nodeType: n.type, position: { x: n.x, y: n.y }, props: n.properties, })), routes: graphData.edges.map(e ({ from: e.sourceNodeId, to: e.targetNodeId, })), }; } function toLogicFlow(serverData) { return { nodes: serverData.nodes.map(n ({ id: n.nodeId, type: n.nodeType, x: n.position.x, y: n.position.y, properties: n.props, })), edges: serverData.routes.map(r ({ sourceNodeId: r.from, targetNodeId: r.to, type: polyline, })), }; }两个函数各管一个方向不要在组件里掺着写。adapter好不好测拿一单真实业务数据先toServer再toLogicFlow再render出来图应该和原来等价。我把这种往返式验证当作每次改动后的强制步骤跑通了才敢提测。6.3 改造的验证方法最终验收我一般走一遍固定清单生产模式打包从后端接口拉一单真实流程走toLogicFlow渲染拖一个新节点、右键置状态、调getGraphData导出把导出结果丢回adapter再渲染一次确认节点数、边数、properties字段一个不漏。这套流程走完业务流程才算闭环。从那以后我每次接入流程编辑器都先把数据适配层写好再碰UI不然改到一半就会陷入前后端数据结构反复拉扯的泥潭。希望帮到你。本文还有配套的精品资源点击获取