jsPlumb + jQuery 流程图设计器实战:连线、数据存储与避坑指南
发布时间:2026/10/14 16:59:59 锦皓数字建站

简介这是一份面向Web前端开发者与工作流设计人员的jsPlumbjQuery流程图设计实例资源适合希望快速掌握可视化连线、拖拽布局与交互式流程图的初中级开发者。压缩包共216个文件约984KB以148张png截图、24个js脚本、20个gif动图为主辅以css样式、config配置及少量htm、cs等文件完整呈现了从端点创建、连接建立到事件监听的实现过程。资源围绕jsPlumb基础API、端点与连接管理、拖拽交互、自定义样式及动画扩展等知识点展开并附带flowchartDemo-master示例项目便于读者对照运行、理解库与jQuery的协同方式。目前已有724人学习下载可作为流程图模块开发、业务拓扑绘制或工作流编辑器的参考实现帮助读者快速搭建可拖拽、可连线的交互式流程图原型。1. 流程图设计器为什么还在用 jsPlumb jQuery 这套老组合打开任何一个工单系统、审批后台或者低代码平台十有八九能看到一张可以拖拽连线、节点能改属性的流程图。很多人第一反应是上 React Flow、X6 或者 G6但如果你接手的是一个已经跑了七八年的老后台页面里塞满了 jQuery 插件构建工具还停留在直接引 script 标签的阶段那 jsPlumb jQuery 反而是阻力最小的选择。它不挑框架、不要求打包、一个 script 引进去就能在 DOM 上画线节点就是普通 div连线靠 SVG 或 Canvas 渲染改起来直观。这套组合解决的核心问题是在已有 jQuery 页面上快速加一层可视化连线能力让业务人员自己拖节点、拉连线、存结构。适合谁适合维护老系统、做内部工具、或者想用最小成本验证流程图交互的产品和前端。下面按「先跑通、再讲参数、最后说坑」的顺序拆开讲。2. 把 jsPlumb 社区版接进 jQuery 页面的最小可跑结构2.1 引入方式与容器约定jsPlumb 社区版community edition是免费开源的直接下载jsplumb.min.js放到静态目录即可不需要 npm。页面结构上所有节点必须放在同一个父容器里父容器要设position: relative否则连线坐标会整体偏移这是最常见的翻车点。!-- 父容器必须 relative节点绝对定位 -- div idflow-canvas styleposition:relative; width:100%; height:600px; border:1px solid #ddd; div classnode idnode-start styleposition:absolute; left:60px; top:80px;开始/div div classnode idnode-approve styleposition:absolute; left:260px; top:80px;审批/div div classnode idnode-end styleposition:absolute; left:460px; top:80px;结束/div /div script srcjquery.min.js/script script srcjsplumb.min.js/script节点用绝对定位是为了让拖拽后坐标可计算jsPlumb 内部靠offset和getBoundingClientRect算端点位置父容器一旦不是定位元素端点就会飘到页面左上角。这一步没做好后面所有连线都是歪的。2.2 初始化实例与端点配置jsPlumb 2.x 之后推荐用jsPlumb.getInstance()拿独立实例避免全局污染。初始化时把容器 id 传进去再给每个节点加端点Endpoint。// 创建独立实例绑定到画布容器 var plumb jsPlumb.getInstance({ Container: flow-canvas, // 限定渲染范围防止连线溢出到页面其他区域 Connector: [Bezier, { curviness: 60 }], // 贝塞尔曲线curviness 控制弯曲程度 PaintStyle: { stroke: #5b7cfa, strokeWidth: 2 }, EndpointStyle: { radius: 5, fill: #5b7cfa }, HoverPaintStyle: { stroke: #ff7a45 } // 悬停高亮方便确认连的是哪根 }); // 给节点加端点源端 目标端 function addEndpoints(nodeId) { plumb.addEndpoint(nodeId, { anchors: [Right, Left], // 右侧出、左侧入 uuid: nodeId -out, isSource: true, isTarget: false, maxConnections: -1 // -1 表示不限制连接数 }); plumb.addEndpoint(nodeId, { anchors: [Left], uuid: nodeId -in, isSource: false, isTarget: true, maxConnections: -1 }); } [node-start, node-approve, node-end].forEach(addEndpoints);anchors决定端点贴在节点的哪条边Right表示右侧中点Left表示左侧中点。uuid必须唯一后面存数据、删连线都靠它定位。maxConnections设成 -1 是允许一个节点连出多条审批流里经常一个节点分叉到多个分支这个参数别写死成 1。2.3 让节点可拖拽并保持连线跟随节点能拖连线才会跟着动。jsPlumb 自带draggable不用再引 jQuery UI。// 让所有 .node 可拖拽限制在画布内 plumb.draggable($(.node), { containment: parent, // 拖拽范围限制在父容器 grid: [10, 10] // 10px 网格吸附对齐更整齐 }); // 监听连线建立事件拿到源和目标 plumb.bind(connection, function (info) { console.log(从, info.sourceId, 连到, info.targetId); // 这里可以把连线关系推入业务数据模型 });containment: parent防止节点被拖出画布导致连线找不到锚点。grid是可选的对齐优化做审批流时节点对齐能省掉大量手动调整。connection事件是数据落库的入口每次连线建立都要在这里把sourceId和targetId记下来否则刷新页面结构就丢了。3. 连线数据怎么存、怎么回显、怎么删3.1 用 connections 序列化出可存储的 JSONjsPlumb 实例上可以拿到所有连线遍历后转成业务能用的结构。// 导出当前所有连线为 JSON function exportFlow() { var conns plumb.getConnections(); var result conns.map(function (c) { return { source: c.sourceId, // 源节点 id target: c.targetId, // 目标节点 id sourceEndpoint: c.endpoints[0].getUuid(), targetEndpoint: c.endpoints[1].getUuid() }; }); return JSON.stringify(result); } // 节点位置也要一起存否则回显时布局全乱 function exportNodes() { return $(.node).map(function () { return { id: this.id, left: parseInt(this.style.left, 10), top: parseInt(this.style.top, 10), text: $(this).text() }; }).get(); }只存连线不存节点坐标回显时节点会堆在左上角连线虽然对但布局没法看。sourceEndpoint和targetEndpoint存下来是为了回显时精确还原端点避免重新计算锚点导致连线方向错乱。3.2 从 JSON 回显流程图回显分两步先按坐标摆好节点再按连线数据重建连接。// 回显先恢复节点位置再重建连线 function loadFlow(nodeData, connData) { nodeData.forEach(function (n) { $(# n.id).css({ left: n.left px, top: n.top px }); }); // 清空旧连线避免重复 plumb.deleteEveryConnection(); connData.forEach(function (c) { plumb.connect({ uuids: [c.sourceEndpoint, c.targetEndpoint], paintStyle: { stroke: #5b7cfa, strokeWidth: 2 } }); }); }deleteEveryConnection在回显前必须调否则多次加载会叠加连线。用uuids而不是source/target来连是因为一个节点可能有多个端点用 uuid 才能精确指定从哪个端点连到哪个端点。3.3 删除连线与节点时的清理顺序删除节点前必须先删掉挂在它上面的所有连线否则 jsPlumb 内部会残留引用控制台报endpoint not found。// 删除节点先删连线再删 DOM function removeNode(nodeId) { plumb.getConnections({ source: nodeId }).concat( plumb.getConnections({ target: nodeId }) ).forEach(function (c) { plumb.deleteConnection(c); }); plumb.removeAllEndpoints(nodeId); // 清掉端点 $(# nodeId).remove(); // 最后删 DOM }顺序反了就会出现「线还在、点没了」的幽灵连线。removeAllEndpoints是 jsPlumb 提供的清理方法比手动遍历端点删除更干净。4. 避坑与排查连线飘、端点丢、性能卡的三类现场4.1 连线整体偏移或飘到页面左上角现象节点明明在画布里连线却画到页面顶部或者偏移几十像素。原因父容器没有position: relative或者页面滚动后没有重新计算偏移。解决给画布容器加position: relative滚动容器内滚动时调用plumb.repaintEverything()重绘。4.2 刷新后端点重复叠加现象反复加载流程图节点上端点越加越多连线出现重影。原因addEndpoint是追加操作没有先清理旧端点。解决初始化前调plumb.reset()或deleteEveryEndpoint()再重新加端点。4.3 节点多了之后拖拽卡顿现象超过 50 个节点后拖拽明显掉帧。原因每次拖拽都触发全量重绘SVG 连线数量大时开销高。解决把Connector从Bezier换成Straight降低计算量或者拖拽时先隐藏连线dragStop后再显示。4.4 连线事件里拿不到最新节点数据现象connection回调里读节点属性拿到的是旧值。原因事件触发时机早于 DOM 更新。解决在回调里用setTimeout(fn, 0)延迟读取或者直接从事件对象的sourceId去数据模型里查不依赖 DOM。4.5 移动端触摸拖拽失效现象PC 上正常手机上节点拖不动。原因jsPlumb 默认监听鼠标事件触摸事件需要额外配置。解决初始化时加plumb.draggable的触摸支持或引入jsplumb的触摸补丁确保touchstart/touchmove被接管。5. 进阶把连线校验和自动布局做进流程设计器基础连线跑通后真正让流程图设计器好用的是两件事连线合法性校验和自动布局。校验解决「业务上不该连的别让它连」自动布局解决「节点一多手动摆太累」。先说校验。jsPlumb 提供beforeDrop钩子在连线落下之前拦截返回false就取消这次连接。plumb.bind(beforeDrop, function (info) { // 规则1不能自己连自己 if (info.sourceId info.targetId) return false; // 规则2结束节点不能作为源 if (info.sourceId node-end) return false; // 规则3同一对节点之间不允许重复连线 var exists plumb.getConnections({ source: info.sourceId, target: info.targetId }); if (exists.length 0) return false; return true; // 通过校验允许建立 });beforeDrop的返回值是布尔true放行、false拦截。规则可以按业务叠加比如审批流里「开始节点只能连出一个」「结束节点只能作为目标」这类约束都写在这里。注意getConnections传对象过滤时source和target要同时给只给一个会返回该节点所有连线。再说自动布局。节点超过二三十个手动拖拽效率极低常见做法是用分层布局算法算坐标再批量设置left/top。下面是一个简化的按层级排布的写法// 简易分层布局按入度为0的节点逐层展开 function autoLayout(nodes, edges) { var inDegree {}; nodes.forEach(function (n) { inDegree[n.id] 0; }); edges.forEach(function (e) { inDegree[e.target] (inDegree[e.target] || 0) 1; }); var layers []; var visited {}; var current nodes.filter(function (n) { return inDegree[n.id] 0; }); while (current.length 0) { layers.push(current); var next []; current.forEach(function (n) { visited[n.id] true; edges.filter(function (e) { return e.source n.id; }) .forEach(function (e) { if (!visited[e.target]) next.push( nodes.find(function (x) { return x.id e.target; }) ); }); }); current next; } // 按层设置坐标每层横向间隔 200纵向间隔 100 layers.forEach(function (layer, li) { layer.forEach(function (n, ni) { $(# n.id).css({ left: (60 li * 200) px, top: (80 ni * 100) px }); }); }); plumb.repaintEverything(); // 布局后必须重绘连线 }这段逻辑的核心是拓扑分层入度为 0 的是第一层去掉第一层后入度变 0 的进第二层依次类推。repaintEverything在坐标批量修改后必须调否则连线还停在旧位置。真实项目里可以直接用 dagre 这类布局库算坐标把结果套进上面的css设置即可思路一样。最后说一个我自己的习惯每次改完连线逻辑先在控制台跑一遍plumb.getAllConnections().length确认连线数量和预期一致再看exportFlow()的 JSON 结构对不对最后才刷新页面验证回显。这个顺序能省掉大量「改了 A 坏了 B」的来回。流程图设计器这类交互密集的东西玄学问题多半出在清理不干净和重绘时机上把这两点盯住基本就稳了。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。