简介:这是一份面向Web前端开发者与流程设计初学者的jsPlumb+jQuery流程图实战示例,重点解决在网页中快速搭建可拖拽、可连线、可交互的流程图这一常见需求。资源包共216个文件,以148张png截图、24个js脚本、20个gif动图为主,辅以css样式、htm/html页面及少量配置与说明文件,压缩后约984KB,体积轻便,便于本地运行与二次改造。内容围绕jsPlumb的初始化、端点添加、连接建立与事件监听展开,并配有flowchartDemo-master完整项目,读者可借此掌握Bezier曲线连接、箭头覆盖物、拖放布局等关键技巧,理解jQuery与jsPlumb协同工作的方式。目前已有724人学习下载,适合希望快速上手流程图交互开发、需要可运行参考代码的前端人员。
1. 从一张“连线永远对不上”的工单说起
前端做流程编排,绕不开一个经典组合:jsPlumb 负责画线,jQuery 负责操作 DOM。很多团队第一次接流程图需求时,都会先搜“jsPlumb jquery 流程图”,然后拿一个 demo 改吧改吧上线。结果节点一多、容器一滚动、页面一缩放,连线就开始飘——明明锚点绑在节点上,线却像脱缰一样挂在半空。这不是 jsPlumb 的锅,是坐标系和重绘时机没对齐。
这份资源就是围绕这个组合做的一套流程图设计实例,核心解决三件事:节点怎么拖、连线怎么连、状态怎么存。它适合两类人:一类是刚接到流程编排需求、想快速跑通一个可交互原型的前端;另一类是已经用过 jsPlumb,但被锚点偏移、连线丢失、序列化还原这些细节反复折磨的熟手。下面我按“能跑起来 → 能改参数 → 能避坑 → 能进阶”的顺序拆一遍,代码可以直接抄,参数我标了含义,坑我踩过的地方会重点说。
2. 环境搭起来:jsPlumb 与 jQuery 的加载顺序和初始化参数
2.1 为什么是 jQuery 打底、jsPlumb 接管连线
jsPlumb 的 DOM 操作依赖一个选择器引擎,早期版本默认绑定 jQuery,后来虽然支持原生和类 jQuery 库,但社区里绝大多数流程图实例仍然是 jQuery + jsPlumb 的组合。原因很实际:节点拖拽用 jQuery UI 的 draggable 最省事,而 jsPlumb 的jsPlumb.draggable()内部就是调 jQuery UI。如果你用原生 drag 事件自己写,拖拽过程中的position更新和 jsPlumb 的repaint时机很难对齐,连线会滞后一帧。
加载顺序不能乱。jQuery 必须在 jsPlumb 之前,jsPlumb 必须在页面 DOM 结构渲染之后初始化。常见做法是在</body>前按顺序引入:
<!-- jQuery 先加载,jsPlumb 内部依赖它做选择器和事件绑定 --> <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script> <!-- jQuery UI 提供 draggable,jsPlumb 的拖拽封装会调用它 --> <script src="https://cdn.jsdelivr.net/npm/jquery-ui@1.13.2/dist/jquery-ui.min.js"></script> <!-- jsPlumb 社区版,注意 2.x 和 5.x 的 API 差异较大 --> <script src="https://cdn.jsdelivr.net/npm/jsplumb@2.15.6/dist/js/jsplumb.min.js"></script>这里有个版本分水岭:jsPlumb 2.x 的jsPlumb.connect()和jsPlumb.addEndpoint()是全局实例方法,5.x 之后推荐用jsPlumb.getInstance()创建独立实例。这份实例用的是 2.x 的写法,因为 jQuery 生态里大量现成代码都是这个版本。如果你要上 5.x,初始化那几行要改成实例调用,后面我会提。
2.2 初始化参数:Container、Connector 和 Endpoint 三件套
jsPlumb 初始化的核心是告诉它“在哪个容器里画、线长什么样、端点长什么样”。这三个配置决定了后面所有连线的默认行为:
// 在 DOM 就绪后初始化,确保容器和节点已经渲染 $(function () { jsPlumb.ready(function () { // 全局默认配置,只设一次 jsPlumb.setContainer($('#flowCanvas')); // 指定画布容器,坐标系基准 var common = { // 连线样式:贝塞尔曲线比直线更不容易和节点重叠 connector: ['Bezier', { curviness: 60 }], // 端点样式:小圆点,半径 6,悬停时放大 endpoint: ['Dot', { radius: 6 }], // 端点位置:节点四边各一个,锚点用百分比定位 anchor: ['Left', 'Right', 'Top', 'Bottom'], // 连线终点样式:箭头 overlays: [['Arrow', { width: 10, length: 10, location: 1 }]], // 允许从端点拖出连线 isSource: true, isTarget: true, // 连线可选中,方便后续删除 connectionsDetachable: true }; // 给每个流程节点批量加端点 $('.flow-node').each(function () { jsPlumb.addEndpoint(this, common); }); }); });setContainer是最容易被忽略的一行。不设容器时,jsPlumb 默认以body为坐标系,一旦画布有position: relative或滚动条,连线就会整体偏移。connector的curviness控制贝塞尔弯曲程度,值越大线越弯,60 是视觉上比较舒服的中间值。anchor用Left/Right/Top/Bottom这种方位词,jsPlumb 会自动算节点边缘的百分比位置,比手写[0.5, 0]更省心。
提示:
jsPlumb.ready()和$(function(){})嵌套时,确保节点 DOM 已经存在。如果是异步渲染的节点,要在节点插入后再调addEndpoint,否则端点会绑到空元素上。
3. 节点拖拽与连线绑定:从静态 DOM 到可交互画布
3.1 用 jsPlumb.draggable 接管拖拽,而不是自己写 mousedown
节点拖拽有两种做法:自己监听mousedown/mousemove/mouseup改left/top,或者直接用jsPlumb.draggable()。前者的问题是拖拽过程中连线不会自动重绘,你得手动调jsPlumb.repaint(),而且频率不好控制,容易掉帧。后者内部封装了 jQuery UI 的 draggable,并在拖拽的drag事件里自动触发连线重绘。
// 让所有流程节点可拖拽,限制在画布容器内 jsPlumb.draggable($('.flow-node'), { containment: 'parent', // 限制在父容器内,防止拖出画布 grid: [10, 10], // 拖拽步长 10px,对齐更整齐 stop: function (event, ui) { // 拖拽结束后手动触发一次重绘,解决部分浏览器下的残影 jsPlumb.repaint(ui.helper); } });containment: 'parent'要求节点的直接父元素是画布容器,如果节点外面还包了一层div,这个限制会失效。grid是可选的对齐辅助,流程节点通常不需要像素级自由拖拽,10px 网格能让布局更规整。stop回调里的repaint是针对某些浏览器在拖拽结束后连线残留旧路径的补救,不是必须,但加上更稳。
3.2 连线建立与删除:connection 事件里的状态同步
连线不是画完就完事,你需要知道“谁连了谁”,才能在保存时序列化、在删除时清理数据。jsPlumb 提供了connection和connectionDetached两个事件:
// 连线建立时,把源和目标记录到节点数据上 jsPlumb.bind('connection', function (info) { var sourceId = info.sourceId; var targetId = info.targetId; // 用 jQuery 的 data 存关联关系,避免全局变量污染 var $source = $('#' + sourceId); var links = $source.data('links') || []; links.push(targetId); $source.data('links', links); console.log('连线建立:', sourceId, '->', targetId); }); // 连线删除时,同步移除记录 jsPlumb.bind('connectionDetached', function (info) { var $source = $('#' + info.sourceId); var links = $source.data('links') || []; var idx = links.indexOf(info.targetId); if (idx > -1) { links.splice(idx, 1); $source.data('links', links); } });info对象里除了sourceId/targetId,还有connection引用,可以通过connection.getUuid()拿到唯一标识。如果你要做“点击连线删除”,就在connection事件里给连线绑定 click:
jsPlumb.bind('connection', function (info) { info.connection.bind('click', function (conn) { jsPlumb.detach(conn); // 删除连线,会触发 connectionDetached }); });这里有个顺序问题:connection事件在连线创建后触发,此时info.connection已经可用;connectionDetached在删除后触发,info.connection仍然指向被删对象,但不能再操作它。状态同步要放在事件回调里做,不要试图在detach之后再去读连线列表。
3.3 序列化与还原:把画布存成 JSON 再读回来
流程图最终要能保存和加载。jsPlumb 本身不提供序列化,需要自己遍历节点位置和连线关系:
// 导出:节点位置 + 连线关系 function exportFlow() { var data = { nodes: [], connections: [] }; $('.flow-node').each(function () { var $node = $(this); data.nodes.push({ id: $node.attr('id'), text: $node.find('.node-label').text(), left: parseInt($node.css('left'), 10), top: parseInt($node.css('top'), 10) }); }); // 遍历所有连线 jsPlumb.getAllConnections().forEach(function (conn) { data.connections.push({ source: conn.sourceId, target: conn.targetId }); }); return JSON.stringify(data); } // 导入:先清空,再重建节点和连线 function importFlow(json) { var data = JSON.parse(json); jsPlumb.deleteEveryConnection(); // 清空连线 jsPlumb.deleteEveryEndpoint(); // 清空端点 $('#flowCanvas').empty(); // 清空节点 DOM // 重建节点 data.nodes.forEach(function (n) { var $node = $('<div class="flow-node" id="' + n.id + '">' + '<span class="node-label">' + n.text + '</span></div>'); $node.css({ left: n.left + 'px', top: n.top + 'px' }); $('#flowCanvas').append($node); }); // 重建端点和连线 $('.flow-node').each(function () { jsPlumb.addEndpoint(this, commonConfig); }); data.connections.forEach(function (c) { jsPlumb.connect({ source: c.source, target: c.target }); }); }deleteEveryConnection和deleteEveryEndpoint的顺序不能反,先删连线再删端点,否则会残留孤立端点。重建时addEndpoint必须在节点插入 DOM 之后调用,jsPlumb.connect必须在端点存在之后调用。这套逻辑跑通,流程图就有了“存档读档”能力。
4. 避坑排查:连线偏移、端点丢失和性能卡顿的五个血泪经验
4.1 现象:连线整体偏移,节点拖到哪线都差一截
原因:画布容器没有设position: relative,或者setContainer指定的元素和实际定位父级不一致。jsPlumb 计算锚点位置时用的是容器坐标系,如果容器本身有margin或padding,偏移量会累积。
解决:给画布容器加position: relative,并确保setContainer传的就是这个元素。如果画布外层还有滚动容器,滚动时要调jsPlumb.repaintEverything()。
4.2 现象:新增节点后端点绑不上,连线拖不出来
原因:节点是异步插入的,addEndpoint执行时 DOM 还没渲染完,选择器选到了空集合。或者节点 class 名写错,$('.flow-node')没匹配到。
解决:在节点插入的回调里再调addEndpoint,不要放在页面初始化的统一循环里。用console.log($('.flow-node').length)确认选择器命中数量。
4.3 现象:拖拽节点时连线闪烁,快速拖动后线跟丢
原因:拖拽过程中repaint频率过高,或者节点上绑了多个 draggable 实例。jQuery UI 的 draggable 重复初始化会导致事件叠加。
解决:拖拽初始化只做一次,用jsPlumb.draggable而不是手动$().draggable()。如果必须手动,先$().draggable('destroy')再重新初始化。
4.4 现象:删除节点后连线还在,或者删除连线后端点残留
原因:只删了 DOM 节点,没有调jsPlumb.remove()。jsPlumb 内部维护了端点和连线的引用,直接$node.remove()不会清理这些引用。
解决:删除节点用jsPlumb.remove($node),它会同时清理该节点上的端点和关联连线。删除连线用jsPlumb.detach(conn),不要只从 DOM 里移除线的 SVG。
4.5 现象:节点超过 50 个后拖拽明显卡顿
原因:每次拖拽都触发全量repaintEverything,连线越多开销越大。另外贝塞尔曲线比直线计算量大。
解决:把connector换成['Straight']或['Flowchart'],减少曲线计算。拖拽时只重绘受影响的连线,用jsPlumb.repaint(nodeId)而不是repaintEverything()。如果节点规模上百,考虑分页或虚拟画布。
5. 进阶技巧:用连接类型和锚点微调把流程图做得更像专业工具
5.1 按连接类型区分样式:条件分支用虚线,主流程用实线
实际流程里,主流程和条件分支的线应该视觉上可区分。jsPlumb 支持在connect时覆盖paintStyle:
// 主流程:实线蓝色 jsPlumb.connect({ source: 'nodeA', target: 'nodeB', paintStyle: { stroke: '#2c7be5', strokeWidth: 2 }, connector: ['Flowchart', { stub: 20 }] }); // 条件分支:虚线橙色 jsPlumb.connect({ source: 'nodeA', target: 'nodeC', paintStyle: { stroke: '#f6a821', strokeWidth: 2, dashstyle: '4 4' }, connector: ['Bezier', { curviness: 80 }] });dashstyle是 SVG 的stroke-dasharray简写,'4 4'表示 4px 实线 4px 空白。Flowchart连接器带直角拐弯,适合主流程;Bezier曲线适合分支,视觉上更柔和。
5.2 锚点微调:用连续锚点解决多线重叠
当一个节点有多条出线时,默认的Right锚点会让所有线从同一点出发,重叠在一起。用Continuous锚点可以让 jsPlumb 自动分配位置:
// 右侧用连续锚点,多条线自动错开 jsPlumb.addEndpoint('nodeA', { anchor: ['Continuous', 'Right', { spacing: 20 }], isSource: true, maxConnections: -1 });spacing: 20表示相邻连线在锚点区域内的间距。maxConnections: -1表示不限制连接数。如果某个端点只允许一条线,设maxConnections: 1,超出时 jsPlumb 会拒绝连接。
5.3 验证方法:用控制台快速检查连线状态
改完参数后,不用每次都点一遍。在控制台跑这几行,能快速确认端点和连线数量:
// 查看所有端点 console.log('端点数量:', jsPlumb.getEndpoints().length); // 查看所有连线 console.log('连线数量:', jsPlumb.getAllConnections().length); // 查看某个节点的连线 console.log('nodeA 的出线:', jsPlumb.getConnections({ source: 'nodeA' }));如果getEndpoints()返回的数量和节点数对不上,说明有节点没绑上端点。如果getAllConnections()比预期多,检查是否有重复connect调用。
5.4 一个我常用的习惯
每次改完connector或anchor配置,我会先在一个只有三个节点的空白画布上跑一遍,确认连线不偏、端点不丢,再套到完整流程里。因为锚点偏移和容器坐标系的问题,在节点少的时候最容易定位,节点一多反而会被其他干扰掩盖。从那以后我每次调 jsPlumb 参数都强制走一遍“三节点验证”,省了很多回头排查的时间。希望帮到你。
本文还有配套的精品资源,点击获取