资讯详情

jsPlumb+jQuery流程图设计实例:节点连线与序列化

📅 2026/10/19 2:47:01 | 华诺云谱 👁 阅读
jsPlumb+jQuery流程图设计实例:节点连线与序列化
简介这是一份面向Web前端开发者与流程可视化初学者的jsPlumbjQuery流程图设计实例资源适合需要快速搭建可拖拽、可连线业务流程图或工作流编辑器的技术人员参考。压缩包共216个文件约984KB以148张png截图、24个js脚本、20个gif动图为主辅以css样式、config配置及少量htm、cs、aspx等示例页面直观呈现端点、连接器、拖放与事件绑定的完整实现。资源围绕jsPlumb基础API、端点创建、连接建立、交互事件监听及flowchartDemo-master项目展开读者可从中获取可运行的流程图Demo源码、样式与动画配置思路以及将流程图与后端数据绑定的扩展方向。目前已有725人学习下载适合对照示例代码理解jsPlumb与jQuery协同工作的关键技巧并在此基础上进行自定义样式与交互优化。1. 从一张“连线画布”说起jsPlumb jQuery 流程图设计实例能解决什么如果你做过审批流配置、工单流转编排、拓扑图编辑这类需求大概率绕不开一个核心问题怎么让用户用鼠标把两个节点连起来还能拖拽、改线、删线、保存结构。纯手写 SVG 或 Canvas 的连线逻辑光是贝塞尔曲线锚点计算和拖拽重绘就能耗掉一周。这套 jsPlumb jQuery 流程图设计实例走的就是另一条路——用 jsPlumb 负责节点之间的连线、锚点、端点管理用 jQuery 负责 DOM 操作和事件绑定两者配合把“画流程图”这件事拆成可维护的模块。它适合两类人一类是刚接触可视化编排、想找一个能跑起来的最小闭环的新手另一类是手里已有业务系统、需要快速嵌入一个流程设计器做二次开发的熟手。下面按“能干什么 → 怎么跑 → 怎么改”的顺序拆开讲。2. 技术选型与整体架构为什么是 jsPlumb 而不是手写 Canvas2.1 jsPlumb 在流程图场景里的定位jsPlumb 本质上是一个“连接管理库”它不负责画节点只负责管理节点之间的连线关系。节点可以是任意 DOM 元素jsPlumb 在这些元素上注册 Endpoint端点然后通过 connect 方法建立连接。它的核心价值在于连线不是静态的 SVG 路径而是有状态的对象可以查询、修改、删除、序列化。这一点对流程图设计器至关重要——用户拖一条线背后要能拿到 source 和 target 的 id才能存进数据库。jQuery 在这里的角色是“胶水”。节点的新增、删除、拖拽初始化、右键菜单、属性面板全部用 jQuery 的 DOM 操作和事件委托完成。两者分工明确jQuery 管节点生命周期jsPlumb 管连线生命周期。常见做法是引入两个文件即可!-- jsPlumb 核心库社区版即可满足流程图需求 -- script srchttps://cdn.jsdelivr.net/npm/jsplumb2.15.6/dist/js/jsplumb.min.js/script !-- jQuery 用于 DOM 操作和事件绑定 -- script srchttps://cdn.jsdelivr.net/npm/jquery3.6.0/dist/jquery.min.js/script提示jsPlumb 社区版和商业版在流程图场景下差异不大社区版足够用。版本选择上2.x 系列 API 稳定不建议用 1.x 的老写法。2.2 整体架构分层这套实例的架构可以拆成四层层级职责关键技术画布层承载节点和连线的容器绝对定位的 div节点层可拖拽的流程节点jQuery UI draggable连线层端点注册与连接管理jsPlumb Instance数据层节点和连线的序列化/反序列化JSON 对象画布层是一个 position: relative 的大 div所有节点用 position: absolute 定位。节点层用 jQuery UI 的 draggable 实现拖拽拖拽结束后更新节点的 left/top 值。连线层在页面初始化时创建 jsPlumb 实例为每个节点添加 Endpoint。数据层负责把当前画布状态导出成 JSON以及从 JSON 还原画布。这种分层的好处是换掉 jQuery UI 用原生 drag 事件或者换掉 jsPlumb 用其他连线库都不会影响其他层。二次开发时改哪层心里有数。2.3 初始化流程与关键配置整个画布的初始化分三步创建 jsPlumb 实例、注册节点端点、绑定拖拽事件。下面是一段可直接跑的初始化代码// 创建 jsPlumb 实例配置连线样式和端点行为 var plumbInstance jsPlumb.getInstance({ // 连线容器的选择器所有连线都渲染在这个容器内 Container: flow-canvas, // 连线两端锚点的默认位置这里用连续锚点自动选择最近边 Anchors: [Continuous, Continuous], // 连线样式贝塞尔曲线颜色和粗细 Connector: [Bezier, { curviness: 50 }], PaintStyle: { stroke: #4a90d9, strokeWidth: 2 }, // 端点样式小圆点悬停时放大 EndpointStyle: { radius: 5, fill: #4a90d9 }, EndpointHoverStyle: { fill: #e74c3c }, // 允许从端点拖出连线 ConnectionsDetachable: true, // 连线可删除 ReattachConnections: true }); // 为画布内所有带 .flow-node 类的节点注册端点 plumbInstance.batch(function () { $(.flow-node).each(function () { var nodeId $(this).attr(id); // 在节点右侧添加源端点左侧添加目标端点 plumbInstance.addEndpoint(nodeId, { anchor: Right, uuid: nodeId -out, isSource: true, isTarget: false, maxConnections: -1 // -1 表示不限制连接数 }); plumbInstance.addEndpoint(nodeId, { anchor: Left, uuid: nodeId -in, isSource: false, isTarget: true, maxConnections: -1 }); }); });这段代码里几个参数值得注意Anchors: [Continuous, Continuous]让 jsPlumb 自动选择节点上最近的锚点位置省去手动计算Connector: [Bezier, { curviness: 50 }]里的 curviness 控制曲线弯曲程度数值越大越弯maxConnections: -1表示一个端点可以接多条线如果业务上要求一个节点只能连一条线改成 1 即可。2.4 节点拖拽与连线跟随节点拖拽用 jQuery UI 的 draggable拖拽过程中 jsPlumb 会自动重绘连线不需要手动调用 repaint。但有一个细节如果节点拖拽时没有触发 jsPlumb 的重绘连线会“掉队”。常见做法是在 draggable 的 drag 回调里调用plumbInstance.repaintEverything()// 初始化节点拖拽 $(.flow-node).draggable({ containment: parent, // 限制在画布内拖拽 grid: [10, 10], // 每 10px 吸附一次对齐更整齐 drag: function () { // 拖拽过程中实时重绘所有连线 plumbInstance.repaintEverything(); }, stop: function () { // 拖拽结束后更新节点位置数据 var nodeId $(this).attr(id); var pos $(this).position(); updateNodePosition(nodeId, pos.left, pos.top); } });containment: parent防止节点被拖出画布grid: [10, 10]让节点按网格吸附视觉上更整齐。repaintEverything()在节点数量多时会有性能开销如果画布上节点超过 50 个建议改成只重绘受影响的连线而不是全量重绘。3. 核心模块拆解节点管理、连线序列化与画布还原3.1 节点的新增与删除新增节点时除了往画布里 append 一个 div还要做三件事初始化 draggable、注册 jsPlumb 端点、绑定删除按钮事件。下面是一个新增节点的函数// 新增一个流程节点 function addNode(nodeId, nodeName, left, top) { // 1. 创建节点 DOM var $node $(div) .addClass(flow-node) .attr(id, nodeId) .css({ left: left px, top: top px }) .html(span classnode-label nodeName /span span classnode-delete×/span); // 2. 插入画布 $(#flow-canvas).append($node); // 3. 初始化拖拽 $node.draggable({ containment: parent, grid: [10, 10], drag: function () { plumbInstance.repaintEverything(); } }); // 4. 注册端点 plumbInstance.addEndpoint(nodeId, { anchor: Right, uuid: nodeId -out, isSource: true, isTarget: false, maxConnections: -1 }); plumbInstance.addEndpoint(nodeId, { anchor: Left, uuid: nodeId -in, isSource: false, isTarget: true, maxConnections: -1 }); // 5. 绑定删除事件 $node.find(.node-delete).on(click, function (e) { e.stopPropagation(); deleteNode(nodeId); }); }删除节点比新增多一步要先断开该节点上的所有连线再移除 DOM最后从数据模型里删掉。如果顺序反了jsPlumb 会报“找不到端点”的错误。// 删除节点及其所有连线 function deleteNode(nodeId) { // 1. 断开该节点上的所有连接 plumbInstance.removeAllEndpoints(nodeId); // 2. 从画布移除 DOM $(# nodeId).remove(); // 3. 从数据模型中删除 delete nodeDataMap[nodeId]; // 4. 重绘画布 plumbInstance.repaintEverything(); }removeAllEndpoints会同时删除端点和挂在端点上的连线这是 jsPlumb 提供的便捷方法。如果只想删连线保留端点用plumbInstance.select({ source: nodeId }).remove()。3.2 连线数据的序列化流程图设计器最终要输出一个可存储的结构。jsPlumb 提供了getAllConnections()方法返回所有连接的数组每个连接对象有 sourceId、targetId、endpointUuid 等属性。下面是一个导出 JSON 的函数// 导出当前画布的所有节点和连线 function exportFlowData() { var nodes []; var connections []; // 遍历所有节点收集位置和名称 $(.flow-node).each(function () { var $node $(this); nodes.push({ id: $node.attr(id), name: $node.find(.node-label).text(), left: parseInt($node.css(left)), top: parseInt($node.css(top)) }); }); // 遍历所有连线收集 source 和 target plumbInstance.getAllConnections().forEach(function (conn) { connections.push({ source: conn.sourceId, target: conn.targetId }); }); return { nodes: nodes, connections: connections }; }导出的结构是{ nodes: [...], connections: [...] }可以直接 JSON.stringify 后存数据库。注意conn.sourceId和conn.targetId是 jsPlumb 自动维护的对应节点的 id 属性不需要手动记录。3.3 从 JSON 还原画布还原画布是序列化的逆过程先清空画布再按 nodes 数组创建节点最后按 connections 数组建立连接。这里有一个坑建立连接时必须等节点端点注册完成否则 jsPlumb 找不到端点。常见做法是用plumbInstance.batch()包住所有操作或者用 setTimeout 延迟连接。// 从 JSON 数据还原画布 function importFlowData(data) { // 1. 清空画布和 jsPlumb 实例 plumbInstance.reset(); $(#flow-canvas).empty(); // 2. 批量创建节点并注册端点 plumbInstance.batch(function () { data.nodes.forEach(function (node) { addNode(node.id, node.name, node.left, node.top); }); }); // 3. 延迟建立连接确保端点已注册 setTimeout(function () { data.connections.forEach(function (conn) { plumbInstance.connect({ uuids: [conn.source -out, conn.target -in], editable: true }); }); }, 100); }plumbInstance.reset()会清空所有端点和连线但不会移除 DOM所以后面还要手动empty()。connect方法用 uuids 指定源端点和目标端点格式是节点id -out和节点id -in和注册时保持一致。3.4 连线的删除与右键菜单用户删除连线有两种方式选中连线按 Delete 键或者右键连线弹出菜单。jsPlumb 支持在连线上绑定 click 事件// 为所有连线绑定点击事件选中后高亮 plumbInstance.bind(connection, function (info) { var conn info.connection; // 点击连线时切换选中状态 conn.bind(click, function () { // 先清除其他连线的选中样式 plumbInstance.getAllConnections().forEach(function (c) { c.setPaintStyle({ stroke: #4a90d9, strokeWidth: 2 }); }); // 高亮当前连线 conn.setPaintStyle({ stroke: #e74c3c, strokeWidth: 3 }); selectedConnection conn; }); }); // 按 Delete 键删除选中的连线 $(document).on(keydown, function (e) { if (e.key Delete selectedConnection) { plumbInstance.deleteConnection(selectedConnection); selectedConnection null; } });connection事件在每次新建连接时触发在这里绑定 click 是最合适的时机。setPaintStyle可以动态修改连线样式用来做选中高亮。deleteConnection删除单条连线不影响端点。4. 避坑与常见问题连线错位、端点丢失、性能卡顿4.1 节点拖拽后连线错位现象拖动节点后连线还停留在原来的位置没有跟随节点移动。原因jsPlumb 默认在窗口 resize 时重绘但节点拖拽是 jQuery UI 控制的jsPlumb 感知不到位置变化。解决在 draggable 的 drag 回调里调用plumbInstance.repaintEverything()。如果节点数量多可以只重绘受影响的连线plumbInstance.repaint(plumbInstance.getConnections({ source: nodeId }))。4.2 动态新增节点后连线连不上现象通过 addNode 新增的节点鼠标从端点拖不出线。原因新增节点后没有调用plumbInstance.addEndpoint或者调用时机在 DOM 插入之前。解决确保 addNode 函数里先 append DOM再 addEndpoint。如果节点是异步加载的用plumbInstance.batch()包住所有端点注册操作。4.3 导入数据后连线全部消失现象调用 importFlowData 后节点显示正常但连线一条都没有。原因plumbInstance.reset()清空了实例但 connect 调用时端点还没注册完成。解决用 setTimeout 延迟 connect或者把 connect 放在plumbInstance.batch()的回调里。更稳妥的做法是监听端点的endpointAdded事件全部注册完成后再建立连接。4.4 画布节点超过 50 个后拖拽卡顿现象节点少时拖拽流畅节点多了以后拖拽明显掉帧。原因repaintEverything()每次拖拽都重绘所有连线复杂度是 O(n)。解决改成只重绘与当前节点相关的连线。另外把Connector从 Bezier 换成 Straight减少曲线计算量。如果还卡考虑用 Canvas 渲染替代 SVG。4.5 连线端点被遮挡无法点击现象两个节点靠得很近时端点被节点 DOM 挡住鼠标点不到。原因节点的 z-index 高于端点。解决给端点设置更高的 z-index或者在节点上设置pointer-events: none让鼠标事件穿透到端点。jsPlumb 的端点默认渲染在画布容器里调整EndpointStyle的 z-index 即可。5. 二次开发改造思路从实例到业务系统的最后一公里这套实例跑通之后真正要嵌入业务系统还有几件事要做。第一件是节点类型的扩展。实例里所有节点长一样实际业务里会有“开始节点”“审批节点”“条件节点”等不同类型每种类型的端点数量、颜色、图标都不同。改造思路是在 addNode 里增加一个 type 参数根据 type 决定注册几个端点、用什么样式。比如条件节点需要两个输出端点分别对应“是”和“否”// 根据节点类型注册不同数量的端点 function registerEndpointsByType(nodeId, type) { if (type condition) { // 条件节点两个输出端点上下分布 plumbInstance.addEndpoint(nodeId, { anchor: Right, uuid: nodeId -out-yes, isSource: true, isTarget: false, paintStyle: { fill: #27ae60 } // 绿色表示“是” }); plumbInstance.addEndpoint(nodeId, { anchor: Bottom, uuid: nodeId -out-no, isSource: true, isTarget: false, paintStyle: { fill: #e74c3c } // 红色表示“否” }); } else { // 普通节点一个输出端点 plumbInstance.addEndpoint(nodeId, { anchor: Right, uuid: nodeId -out, isSource: true, isTarget: false }); } // 所有节点都有输入端点 plumbInstance.addEndpoint(nodeId, { anchor: Left, uuid: nodeId -in, isSource: false, isTarget: true }); }第二件是连线的业务校验。不是所有节点之间都能连线比如“开始节点”不能有输入“结束节点”不能有输出。jsPlumb 提供了connection事件的拦截能力可以在建立连接前校验// 在连接建立前校验合法性 plumbInstance.bind(connection, function (info) { var sourceType getNodeType(info.sourceId); var targetType getNodeType(info.targetId); // 开始节点不能作为目标结束节点不能作为源 if (targetType start || sourceType end) { plumbInstance.deleteConnection(info.connection); alert(该连接不合法); return false; } });第三件是撤销/重做。这是流程图设计器里最容易被低估的功能。实现思路是每次操作后把 exportFlowData 的结果压入一个栈撤销时弹出上一个状态并调用 importFlowData。注意要限制栈的深度否则内存会涨。我一般会限制 50 步超过就丢弃最早的记录。最后说一个我踩过的坑jsPlumb 的reset()方法不会重置实例的配置如果之前改过 PaintStylereset 后样式还是改过的。所以每次 importFlowData 之前最好重新创建一个 jsPlumb 实例而不是复用旧的。从那以后我每次做画布还原都强制走一遍“销毁实例 → 重建实例 → 注册端点 → 建立连接”的完整流程再也没出现过样式串台的问题。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑