
简介这是一份面向Web前端开发者与流程设计初学者的jsPlumbjQuery流程图设计实例资源重点解决在网页中快速搭建可拖拽、可连线、可交互的流程图这一常见需求。压缩包共216个文件约984KB以148张png截图、24个js脚本、20个gif动图为主辅以css样式、config配置及少量htm、cs、aspx等示例页面完整呈现了从端点创建、连接建立到事件监听的实现过程。资源内含flowchartDemo-master项目涵盖Bezier曲线连接、箭头覆盖物、拖放调整布局、连接创建事件监听等关键知识点并展示了自定义样式、动画效果与数据绑定等扩展方向。目前已有725人学习下载适合希望掌握jsPlumb与jQuery协同工作方式、需要可运行示例对照练习的开发者参考。1. 为什么今天还值得翻出 jsPlumb jQuery 这套老组合上周帮一个做内部工单系统的朋友改需求他甩过来一个压缩包说“流程图这块卡了三天你帮我看看”。打开一看jsPlumb 配 jQuery代码风格还是十年前的写法但功能跑得挺稳。这让我想起一个反直觉的事实在流程图设计器这个场景里很多团队绕了一圈 React Flow、X6、LogicFlow最后又回到 jsPlumb原因很简单——它不挑框架jQuery 能跑原生 JS 也能跑甚至 Vue 里塞进去也不打架。这份资源就是一套 jsPlumb jQuery 的流程图设计实例核心解决的是“在网页上拖拽节点、连线、保存拓扑结构”这件事。它适合两类人一类是维护老系统的前端手上就是 jQuery 技术栈不想为了一个流程图引入整套构建工具另一类是想理解流程图底层交互原理的人jsPlumb 的 API 足够直白能把“锚点、端点、连接、覆盖物”这些概念拆开看。下面我按实际拆包和改造的过程把选型理由、核心配置、踩坑记录和进阶技巧一次讲清。2. jsPlumb 核心概念与 jQuery 集成方式先搞懂四个对象再动手2.1 端点、锚点、连接、覆盖物的关系jsPlumb 的模型里最容易混淆的是“端点”和“锚点”。我一般这样理解锚点是节点边缘上的一个“接口位置”比如右边缘中点、上边缘三分之一处端点是挂在这个锚点上的“插头”它决定了连线的样式、类型和可拖拽性。一个节点可以声明多个锚点每个锚点可以挂一个端点连线就是从源端点拉到目标端点。连接Connection是端点之间的那条线它有自己的样式和事件。覆盖物Overlay是挂在连接或端点上的附加元素比如箭头、标签、删除按钮。这四个对象的关系是节点 → 锚点 → 端点 → 连接 → 覆盖物。搞清这条链后面配置参数时就不会把anchor和endpoint写混。jQuery 在这里的角色主要是 DOM 操作和事件绑定。jsPlumb 本身不依赖 jQuery但用 jQuery 选元素、绑拖拽、读属性会顺手很多。常见做法是用 jQuery 初始化节点位置和拖拽用 jsPlumb 管理连线和端点两者通过 DOM 元素引用对接。2.2 初始化 jsPlumb 实例的推荐参数直接上代码这是我改造后稳定跑起来的初始化配置// 初始化 jsPlumb 实例指定容器 var plumbInstance jsPlumb.getInstance({ Container: flow-canvas, // 限定连线范围避免线跑到画布外 Connector: [Bezier, { curviness: 50 }], // 贝塞尔曲线curviness 控制弯曲度 Endpoint: [Dot, { radius: 5 }], // 端点样式圆点半径 5px EndpointStyle: { fill: #4a90d9, outlineWidth: 2, outlineStroke: #fff }, PaintStyle: { stroke: #4a90d9, strokeWidth: 2 }, // 连线样式 HoverPaintStyle: { stroke: #f5a623, strokeWidth: 3 }, // 悬停高亮 ConnectionOverlays: [ [Arrow, { location: 1, width: 10, length: 10 }], // 箭头在终点 [Label, { location: 0.5, label: , cssClass: conn-label }] // 中间标签 ], Anchors: [RightMiddle, LeftMiddle], // 默认锚点右出左进 ReattachConnections: true, // 允许重新拖拽连线端点 ConnectionsDetachable: true // 允许删除连线 });这段配置里几个参数值得展开。Container一定要设否则连线会以整个页面为坐标系滚动时线会飘。Connector选 Bezier 而不是 Straight是因为流程图里节点位置经常错开直线容易重叠贝塞尔曲线视觉上更清晰。curviness我一般设 30 到 80太小接近直线太大曲线会绕远。Anchors默认设成右出左进符合从左到右的阅读习惯但实际项目里要根据节点类型动态改。ReattachConnections和ConnectionsDetachable这两个开关建议都打开前者让用户能拖动连线端点改接到别的节点后者允许点选连线后删除。关掉的话交互会显得很“死”用户会来提需求。2.3 用 jQuery 驱动节点拖拽与端点绑定节点拖拽用 jQuery UI 的 draggable 最省事但要注意拖拽过程中连线要跟着动。jsPlumb 提供了plumbInstance.draggable()方法它内部会处理重绘比手动调repaint()靠谱。// 用 jQuery 选中所有节点初始化拖拽 $(.flow-node).each(function () { var $node $(this); var nodeId $node.attr(id); // 让 jsPlumb 接管拖拽自动重绘连线 plumbInstance.draggable(nodeId, { containment: parent, // 限制在画布内 grid: [10, 10] // 10px 网格吸附对齐更整齐 }); // 为每个节点添加源端点和目标端点 plumbInstance.addEndpoint(nodeId, { anchor: RightMiddle, uuid: nodeId -source, isSource: true, maxConnections: -1 // -1 表示不限制连接数 }); plumbInstance.addEndpoint(nodeId, { anchor: LeftMiddle, uuid: nodeId -target, isTarget: true, maxConnections: -1 }); });这里uuid是端点的唯一标识后面保存拓扑结构时靠它来记录“谁连了谁”。maxConnections设 -1 表示不限制但实际业务里一个审批节点通常只允许一条出线那就设成 1超了 jsPlumb 会自动阻止并触发connectionAborted事件可以在事件里给用户提示。grid参数是容易被忽略的细节。流程图节点如果随意摆放连线会显得很乱加 10px 网格吸附后节点位置自动对齐视觉上规整很多。代价是拖拽时有一点“顿挫感”但大多数用户能接受。2.4 连线建立与数据持久化的衔接连线建立后要把拓扑结构存下来。jsPlumb 提供getConnections()方法返回所有连接对象每个连接有sourceId、targetId和getUuid()。我一般存成 JSON 数组// 收集当前所有连线数据 function collectTopology() { var connections plumbInstance.getConnections(); var topology connections.map(function (conn) { return { source: conn.sourceId, // 源节点 DOM id target: conn.targetId, // 目标节点 DOM id sourceEndpoint: conn.endpoints[0].getUuid(), targetEndpoint: conn.endpoints[1].getUuid() }; }); return topology; } // 页面加载时恢复连线 function restoreTopology(topology) { topology.forEach(function (item) { plumbInstance.connect({ uuids: [item.sourceEndpoint, item.targetEndpoint], editable: true }); }); }connect()方法用uuids参数恢复连线时前提是端点已经用相同的 uuid 创建好了。所以恢复顺序是先渲染节点 DOM再创建端点最后调connect()。如果顺序反了会报“端点不存在”的错。这个坑我在三个项目里都遇到过后面会细说。3. 从零搭一个可保存的流程图设计器节点渲染、连线校验与数据回显3.1 节点模板与动态渲染节点不要写死在 HTML 里用 jQuery 动态生成方便从后端拉数据。我一般定义一个renderNode(nodeData)函数// nodeData: { id, name, type, x, y } function renderNode(nodeData) { var $node $(div) .addClass(flow-node) .attr(id, nodeData.id) .attr(data-type, nodeData.type) .css({ left: nodeData.x px, top: nodeData.y px }) .html( div classnode-header nodeData.name /div div classnode-body span classnode-type nodeData.type /span /div ); $(#flow-canvas).append($node); return $node; }节点 DOM 的id直接用作 jsPlumb 的节点标识所以必须唯一。>plumbInstance.bind(beforeDrop, function (info) { var sourceType $(# info.sourceId).attr(data-type); var targetType $(# info.targetId).attr(data-type); // 规则开始节点不能作为目标结束节点不能作为源 if (targetType start) { alert(开始节点不能有入线); return false; // 返回 false 阻止连线建立 } if (sourceType end) { alert(结束节点不能有出线); return false; } // 规则同类型节点不允许直连 if (sourceType targetType) { alert(同类型节点不能直连); return false; } return true; // 返回 true 允许建立 });beforeDrop的info对象包含sourceId、targetId、connection等字段。返回false时 jsPlumb 会丢弃这条连线但不会自动删除已经拖出来的临时线需要手动调plumbInstance.detach(info.connection)清理。这个细节不处理的话用户会看到一条“幽灵线”挂在画布上。校验规则建议抽成独立函数用配置表驱动而不是写一堆 if-else。比如var connectionRules { start: { allowOut: [task, decision], allowIn: [] }, task: { allowOut: [task, decision, end], allowIn: [start, task, decision] }, decision: { allowOut: [task, end], allowIn: [task, decision] }, end: { allowOut: [], allowIn: [task, decision] } };这样加新节点类型时只改配置不动逻辑。3.3 保存与回显的完整链路保存分两步存节点位置和存连线拓扑。节点位置在拖拽停止时更新plumbInstance.bind(dragStop, function (el) { var $el $(el); var nodeId $el.attr(id); // 更新内存中的节点数据 var nodeData nodeMap[nodeId]; nodeData.x parseInt($el.css(left), 10); nodeData.y parseInt($el.css(top), 10); });nodeMap是一个以节点 id 为 key 的对象保存所有节点的业务数据。拖拽停止时同步坐标保存时直接序列化nodeMap和collectTopology()的结果。回显时按“清空画布 → 渲染节点 → 创建端点 → 恢复连线”的顺序执行。清空画布要调plumbInstance.reset()它会删除所有端点和连线但不会删除 DOM 节点所以还要手动$(#flow-canvas).empty()。顺序反了会残留端点导致恢复连线时 uuid 冲突。function loadFlow(data) { plumbInstance.reset(); $(#flow-canvas).empty(); nodeMap {}; data.nodes.forEach(function (node) { nodeMap[node.id] node; renderNode(node); }); // 端点创建要在所有节点渲染完之后 data.nodes.forEach(function (node) { addNodeEndpoints(node.id); }); // 最后恢复连线 restoreTopology(data.connections); }这套链路跑通后刷新页面、切换标签、从后端拉数据都能正常回显。我一般会在restoreTopology里加一个setTimeout延迟 0 毫秒执行等 DOM 完全渲染后再连线避免偶发的“端点未找到”报错。4. 避坑排查连线漂移、端点重复与性能衰减的五个血泪经验4.1 画布滚动后连线错位现象页面滚动或画布容器滚动后连线端点跟节点对不上线飘到别处。原因jsPlumb 默认以页面为坐标系计算端点位置容器滚动后偏移量没更新。解决初始化时指定Container为画布元素并在滚动事件里调plumbInstance.repaintEverything()。如果画布本身不滚动而是外层页面滚动给画布设position: relative并确保 jsPlumb 的 Container 指向它。4.2 重复初始化导致端点叠加现象节点上出现多个端点圆点连线时不知道该连哪个或者一条线拖出来变成两条。原因在循环或多次触发的函数里调用了jsPlumb.getInstance()或addEndpoint()同一个节点被加了多次端点。解决全局只保留一个 jsPlumb 实例用变量缓存。addEndpoint()前先检查该节点是否已有端点可以用plumbInstance.getEndpoints(nodeId)判断长度大于 0 就跳过。4.3 大量节点时拖拽卡顿现象节点超过 50 个后拖拽明显掉帧连线重绘延迟。原因每次拖拽都触发全量repaintEverything()计算量随节点数线性增长。解决改用plumbInstance.draggable()自带的增量重绘不要手动绑drag事件调repaint。另外把Connector从 Bezier 换成 Straight 或 Flowchart曲线计算比直线贵不少。如果节点上百考虑分页或懒加载别硬扛。4.4 删除节点后连线残留现象删掉一个节点 DOM但连在它上面的线还挂在画布上点也点不掉。原因只删了 DOM没有通知 jsPlumb 清理关联的端点和连接。解决删除节点前先调plumbInstance.remove(nodeId)它会自动删除该节点上的所有端点和连接。然后再$(# nodeId).remove()。顺序不能反反了 jsPlumb 找不到节点会报错。4.5 保存的拓扑恢复后连线样式丢失现象保存前连线有箭头和标签恢复后变成光秃秃的线。原因connect()恢复时没有传overlays参数jsPlumb 用了默认样式。解决恢复连线时把覆盖物配置一起传进去或者提前用plumbInstance.importDefaults()设好全局默认覆盖物。我一般把覆盖物配置抽成常量初始化和恢复时共用同一份。5. 进阶技巧用 overlay 做连线标签编辑与拓扑导出校验5.1 连线标签的双击编辑流程图里经常需要在连线上标注条件比如“同意”“驳回”。jsPlumb 的 Label overlay 默认是静态文本要支持双击编辑得自己绑事件plumbInstance.bind(connection, function (info) { var conn info.connection; // 给连线添加双击事件 conn.bind(dblclick, function (connection) { var currentLabel ; var overlays connection.getOverlays(); // 找到 Label 类型的 overlay for (var id in overlays) { if (overlays[id].type Label) { currentLabel overlays[id].getLabel(); break; } } var newLabel prompt(输入连线标签, currentLabel); if (newLabel ! null) { connection.setLabel(newLabel); } }); });connection.setLabel()会更新 Label overlay 的文本。注意getOverlays()返回的是对象key 是 overlay 的 id需要遍历找到 type 为 Label 的那个。如果连线有多个 Label overlay这段逻辑只改第一个实际项目里可以给每个 overlay 设不同 id 来区分。5.2 拓扑导出的完整性校验保存前做一次校验防止出现孤立节点或断头线。我一般检查三件事是否有节点没有任何连线、是否有连线指向不存在的节点、开始和结束节点是否成对出现。function validateTopology(nodes, connections) { var errors []; var nodeIds nodes.map(function (n) { return n.id; }); var connectedIds {}; connections.forEach(function (conn) { connectedIds[conn.source] true; connectedIds[conn.target] true; if (nodeIds.indexOf(conn.source) -1) { errors.push(连线源节点不存在: conn.source); } if (nodeIds.indexOf(conn.target) -1) { errors.push(连线目标节点不存在: conn.target); } }); nodes.forEach(function (node) { if (!connectedIds[node.id] node.type ! start node.type ! end) { errors.push(孤立节点: node.name); } }); var startCount nodes.filter(function (n) { return n.type start; }).length; var endCount nodes.filter(function (n) { return n.type end; }).length; if (startCount ! 1 || endCount ! 1) { errors.push(开始或结束节点数量异常); } return errors; }校验不通过时把errors数组展示给用户让他们修正后再保存。这个函数在保存按钮的点击事件里调比等到后端报错再返回友好得多。5.3 用 importDefaults 统一管理样式项目里连线样式如果散落在各处改一个颜色要翻十几个地方。我习惯在初始化后用importDefaults设全局默认值plumbInstance.importDefaults({ PaintStyle: { stroke: #4a90d9, strokeWidth: 2 }, HoverPaintStyle: { stroke: #f5a623, strokeWidth: 3 }, EndpointStyle: { fill: #4a90d9, radius: 5 }, ConnectionOverlays: [ [Arrow, { location: 1, width: 10, length: 10 }] ] });这样新增连线时自动套用默认样式个别连线要改样式再单独setPaintStyle()。importDefaults只影响之后创建的连接已有的连接不受影响所以要在初始化阶段尽早调用。5.4 一个我踩过的坑reset 之后事件绑定丢失plumbInstance.reset()会清除所有端点和连接但通过plumbInstance.bind()绑定的全局事件比如beforeDrop、connection不会丢它们是绑在实例上的。但如果你在节点 DOM 上绑了 jQuery 事件比如点击节点弹出属性面板$(#flow-canvas).empty()会把这些 DOM 一起删掉事件自然也没了。所以重新渲染节点后要重新绑节点级事件。我一般把节点事件绑定封装在renderNode里每次渲染都重新绑避免遗漏。从那以后我每次做流程图保存回显都强制走一遍“清空 → 渲染 → 绑事件 → 建端点 → 恢复连线 → 校验”的完整流程不再图省事跳过任何一步。希望帮到你。本文还有配套的精品资源点击获取