☰
jsPlumb + jQuery 流程图设计器:拖拽连线与序列化实战
2026/10/19 2:46:40 网站建设 项目流程

简介:这是一份面向Web前端开发者与工作流设计人员的jsPlumb+jQuery流程图设计实例资源,适合希望快速掌握可视化连线、节点拖拽与动态流程图搭建的初中级开发者。压缩包共216个文件,约984KB,以148张png截图、24个js脚本、20个gif动图为主,辅以css样式、config配置及少量cs、aspx等源码文件,完整呈现了一个可运行的流程图演示项目。资源围绕jsPlumb的端点创建、连接器样式、事件监听与jQuery的DOM操作展开,包含Bezier曲线、箭头覆盖物、拖放交互等典型实现,并附带flowchartDemo-master示例工程,便于读者对照源码理解节点连接、连接创建与删除事件、自定义样式及动画扩展等关键技巧。目前已有725人学习下载,适合作为流程图组件开发、业务拓扑图搭建的入门参考与二次开发基础。

1. 从一张审批单说起:为什么 jsPlumb + jQuery 仍是流程图设计的高性价比方案

很多做后台系统的同学都遇到过这样的需求:业务方要一个能拖拽节点、连线、保存拓扑关系的流程图设计器,用来配置审批流、任务依赖或者数据管道。第一反应往往是上 React Flow 或者 G6,但真到落地时才发现,老项目是 jQuery 技术栈,引入一整套前端框架的成本比想象中高得多。这时候 jsPlumb + jQuery 的组合就显出价值了——它不要求你重构整个页面,只要在现有 DOM 上挂载连线能力,几百行代码就能跑出一个可用的流程图设计实例。这套方案特别适合课程设计、毕设里需要「可视化配置」模块的场景,也适合维护存量 jQuery 项目的工程师快速交付一个流程图编辑器。核心要解决的问题就三个:节点可拖拽、连线可建立与删除、拓扑结构可序列化保存。

2. 需求拆解与技术选型:为什么是 jsPlumb 而不是 Canvas 自绘

2.1 功能边界先划清楚

在动手写代码之前,我一般会先把流程图设计器的功能边界列出来,避免做到一半发现漏了关键能力。这个实例需要覆盖的核心功能包括:

  • 左侧物料区提供三种节点类型(开始、处理、结束),拖拽到画布生成节点
  • 节点在画布内可自由拖动,拖动时连线跟随移动
  • 节点边缘有连接锚点,从一个节点拖到另一个节点建立连线
  • 连线可选中并删除,节点删除时关联连线一并清除
  • 画布支持保存为 JSON 结构,包含节点坐标、类型、ID 和连线关系
  • 支持从 JSON 反向渲染,用于编辑已有流程

这些功能看起来简单,但每一项都有坑。比如节点拖动时连线跟随,如果用 Canvas 自绘,需要自己算贝塞尔曲线控制点;用 jsPlumb 的话,它内部已经处理了重绘逻辑,你只需要调用repaint方法。

2.2 技术选型对比:Canvas 自绘 vs SVG vs jsPlumb

选型这件事,我的习惯是先看团队技术栈和交付周期。如果项目本身就是 jQuery 体系,硬上 React Flow 意味着要引入构建工具、状态管理、组件通信一整套东西,对于一个小型流程图模块来说属于过度设计。下面这张表是我在几个方案之间做的对比:

方案开发成本与 jQuery 兼容性连线交互序列化难度
Canvas 自绘高好需自己实现中
SVG 手写中高好需自己实现中
jsPlumb + jQuery低极好内置低
React Flow中差内置低

jsPlumb 的本质是在 DOM 元素上叠加一层 SVG 或 Canvas 来绘制连线,节点本身还是普通 div,所以和 jQuery 的拖拽、事件绑定天然契合。它的社区版功能已经覆盖了流程图设计器 90% 的需求,包括锚点、端点、连接器样式、拖拽重绘等。

2.3 依赖引入与初始化配置

引入 jsPlumb 有两种方式:直接 script 标签引入,或者通过 npm 安装。对于 jQuery 项目,我通常直接用 script 标签,避免构建工具的折腾。

<!-- 先引入 jQuery,再引入 jsPlumb --> <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jsplumb@2.15.6/dist/jsplumb.min.js"></script>

初始化 jsPlumb 实例时,有几个关键配置项需要提前设好,否则后面改起来很麻烦:

// 初始化 jsPlumb 实例,绑定到画布容器 var instance = jsPlumb.getInstance({ Container: "flowCanvas", // 画布容器 ID,所有连线绘制在这个容器内 Connector: ["Bezier", { curviness: 50 }], // 连线样式:贝塞尔曲线,弯曲度 50 Endpoint: ["Dot", { radius: 4 }], // 端点样式:圆点,半径 4px Anchor: "Continuous", // 锚点自动选择最近边缘 PaintStyle: { stroke: "#456", strokeWidth: 2 }, // 连线颜色和粗细 HoverPaintStyle: { stroke: "#2a7", strokeWidth: 3 }, // 悬停高亮 ConnectionOverlays: [ // 连线上的叠加元素 ["Arrow", { location: 1, width: 10, length: 10 }] // 箭头 ] });

这里Container必须指定,否则连线会挂到 body 上,滚动页面时连线位置会错乱。Anchor: "Continuous"让 jsPlumb 自动选择节点最近的边缘作为连接点,省去手动指定锚点位置的麻烦。ConnectionOverlays里的箭头配置让连线有方向感,审批流这类场景必须要有。

3. 总体架构设计:四个模块撑起一个流程图编辑器

3.1 模块划分与职责

整个流程图设计器我拆成四个模块,每个模块职责单一,方便调试和替换:

  • 物料区模块:负责渲染可拖拽的节点模板,处理拖拽起始事件,把节点类型传递给画布
  • 画布模块:负责接收拖拽生成节点、管理节点 DOM、初始化 jsPlumb 端点
  • 连线管理模块:负责建立连线、删除连线、校验连线合法性(比如开始节点不能有入边)
  • 数据序列化模块:负责把画布状态导出为 JSON,以及从 JSON 恢复画布

这四个模块之间通过自定义事件通信,而不是直接互相调用。比如物料区拖拽结束时触发node:drop事件,画布模块监听这个事件并创建节点。这样做的好处是模块可以独立测试,也方便后续替换某个模块的实现。

3.2 节点数据结构设计

节点和连线的数据结构直接决定了序列化的难易程度。我一般用下面这种扁平结构:

// 节点对象 { id: "node_1", // 唯一标识,用时间戳或递增数字 type: "process", // 节点类型:start / process / end label: "审批", // 显示文本 x: 300, // 相对画布的 X 坐标 y: 150 // 相对画布的 Y 坐标 } // 连线对象 { id: "conn_1", // 连线唯一标识 source: "node_1", // 起点节点 ID target: "node_2", // 终点节点 ID sourceAnchor: [0.5, 1], // 起点锚点位置(可选,用于精确恢复) targetAnchor: [0.5, 0] // 终点锚点位置 }

保存时把节点数组和连线数组一起序列化,恢复时先创建节点 DOM,再根据连线数据调用instance.connect重建连线。这里有个细节:恢复连线时如果锚点位置不保存,jsPlumb 会重新计算,可能导致连线走向和保存前不一致。所以我在连线对象里额外存了锚点坐标。

3.3 画布与物料区的 DOM 结构

DOM 结构尽量简单,避免深层嵌套导致拖拽计算复杂:

<div class="flow-editor"> <!-- 左侧物料区 --> <div class="palette"> <div class="palette-item">// 物料区:拖拽开始,把节点类型写入 dataTransfer $('.palette-item').on('dragstart', function (e) { e.originalEvent.dataTransfer.setData('nodeType', $(this).data('type')); }); // 画布:允许放置 $('#flowCanvas').on('dragover', function (e) { e.preventDefault(); // 必须阻止默认行为才能触发 drop }); // 画布:放置时创建节点 $('#flowCanvas').on('drop', function (e) { e.preventDefault(); var type = e.originalEvent.dataTransfer.getData('nodeType'); if (!type) return; // 计算节点在画布内的坐标 var offset = $(this).offset(); var x = e.originalEvent.clientX - offset.left - 40; // 减去节点半宽,让节点居中 var y = e.originalEvent.clientY - offset.top - 20; createNode(type, x, y); });

createNode函数负责生成节点 DOM 并注册 jsPlumb 端点:

var nodeIdCounter = 0; function createNode(type, x, y) { var id = 'node_' + (++nodeIdCounter); var label = { start: '开始', process: '处理', end: '结束' }[type]; // 创建节点 DOM var $node = $('<div class="flow-node"></div>') .attr('id', id) .addClass('node-' + type) .css({ left: x, top: y }) .text(label); $('#flowCanvas').append($node); // 注册 jsPlumb 端点:源端和目标端都注册,允许双向连线 instance.addEndpoint(id, { uuid: id + '_source', anchor: 'Continuous', isSource: true, isTarget: true, maxConnections: -1 // -1 表示不限制连接数 }); // 节点自身可拖动 instance.draggable(id, { containment: 'parent', // 限制在画布内拖动 grid: [10, 10] // 拖动时按 10px 网格吸附,对齐更整齐 }); return id; }

这里instance.draggable是 jsPlumb 提供的拖拽方法,它内部用 jQuery UI 的 draggable 实现,但不需要你额外引入 jQuery UI。containment: 'parent'确保节点不会被拖出画布,grid让节点对齐到网格,视觉上更规整。

4.2 连线建立与删除

连线建立是 jsPlumb 自动处理的,你只需要在初始化时配置好端点。但删除连线需要手动绑定事件:

// 连线建立后触发,可以在这里做合法性校验 instance.bind('connection', function (info) { var sourceType = $('#' + info.sourceId).data('type'); var targetType = $('#' + info.targetId).data('type'); // 开始节点不能有入边,结束节点不能有出边 if (targetType === 'start' || sourceType === 'end') { instance.deleteConnection(info.connection); alert('开始节点不能作为终点,结束节点不能作为起点'); return; } // 保存连线数据到内存 connectionList.push({ id: 'conn_' + info.connection.id, source: info.sourceId, target: info.targetId }); }); // 点击连线选中,按 Delete 键删除 instance.bind('click', function (conn, originalEvent) { if (confirm('确定删除这条连线吗?')) { instance.deleteConnection(conn); // 从内存中移除 connectionList = connectionList.filter(function (c) { return c.id !== 'conn_' + conn.id; }); } });

connection事件的info参数包含sourceId、targetId和connection对象。合法性校验放在这里最合适,因为此时连线已经建立但还未持久化,删除操作对用户来说是无感的。click事件绑定在 instance 上,点击连线时触发,用confirm确认删除,避免误操作。

4.3 序列化与反序列化

保存功能把节点和连线数据导出为 JSON:

function saveFlow() { var nodes = []; $('#flowCanvas .flow-node').each(function () { var $node = $(this); nodes.push({ id: $node.attr('id'), type: $node.data('type'), label: $node.text(), x: parseInt($node.css('left')), y: parseInt($node.css('top')) }); }); var data = { nodes: nodes, connections: connectionList }; // 输出到控制台或发送到后端 console.log(JSON.stringify(data, null, 2)); return data; }

恢复时先清空画布,再按顺序重建:

function loadFlow(data) { // 清空画布和 jsPlumb 实例 instance.reset(); $('#flowCanvas').empty(); connectionList = []; nodeIdCounter = 0; // 先创建所有节点 data.nodes.forEach(function (node) { var id = createNode(node.type, node.x, node.y); // 恢复节点 ID 和标签 $('#' + id).attr('id', node.id).text(node.label); // 更新 nodeIdCounter,避免后续创建节点 ID 冲突 var num = parseInt(node.id.replace('node_', '')); if (num > nodeIdCounter) nodeIdCounter = num; }); // 再重建连线 data.connections.forEach(function (conn) { instance.connect({ source: conn.source, target: conn.target, anchors: [conn.sourceAnchor || 'Continuous', conn.targetAnchor || 'Continuous'] }); connectionList.push(conn); }); }

这里有个容易翻车的点:instance.reset()会清除所有端点和连线,但不会清除 DOM 节点,所以需要手动empty()。另外节点 ID 恢复后,nodeIdCounter要同步更新,否则下次创建节点时 ID 会重复,导致 jsPlumb 端点冲突。

5. 避坑与常见问题排查:那些让我加班到凌晨的细节

5.1 连线位置偏移或错乱

现象:拖动节点后连线不跟随,或者连线整体偏移到画布外。

原因:画布容器没有设置position: relative,jsPlumb 的连线层以 body 为定位基准。或者画布有滚动条时,没有调用instance.repaintEverything()。

解决:给画布容器加position: relative,并在画布滚动或窗口 resize 时调用instance.repaintEverything()。如果画布内有滚动,还需要在滚动事件里重新计算偏移。

5.2 节点拖动时连线闪烁

现象:拖动节点过程中,连线频繁重绘导致闪烁。

原因:jsPlumb 默认在拖动时实时重绘,如果节点数量多,性能跟不上。

解决:在instance.draggable的配置里加drag: function () { instance.repaintEverything(); },或者降低重绘频率。更彻底的办法是拖动时隐藏连线,拖动结束后再显示,但体验会打折扣。

5.3 序列化后恢复,连线锚点位置不对

现象:保存前连线从节点右侧出发,恢复后变成从底部出发。

原因:Anchor: "Continuous"是动态计算的,恢复时节点位置和保存时一致,但 jsPlumb 内部状态不同,导致锚点重新计算。

解决:在连线数据里保存sourceAnchor和targetAnchor的具体坐标,恢复时用anchors参数指定,而不是用Continuous。

5.4 删除节点后连线残留

现象:删除节点 DOM 后,连线还在画布上。

原因:只删了 DOM,没有调用instance.remove清除 jsPlumb 内部记录。

解决:删除节点时先调用instance.remove(nodeId),再移除 DOM。instance.remove会自动删除该节点关联的所有连线和端点。

5.5 多个 jsPlumb 实例冲突

现象:页面上有多个画布时,连线跑到另一个画布上。

原因:jsPlumb 默认是单例模式,多次getInstance返回同一个实例。

解决:用jsPlumb.getInstance创建独立实例,并给每个实例指定不同的Container。如果确实需要多个实例,确保它们的容器 ID 不重复。

6. 验收与答辩:怎么证明你的流程图设计器是合格的

验收这个流程图设计器,我一般从五个维度来检查,每个维度都有具体的操作步骤和预期结果:

验收项操作步骤预期结果
节点生成从物料区拖拽三种节点到画布节点出现在鼠标释放位置,样式正确
节点拖动拖动任意节点到画布另一位置节点跟随鼠标,连线实时重绘,不超出画布
连线建立从一个节点端点拖到另一个节点生成带箭头的贝塞尔曲线,方向正确
连线删除点击连线,确认删除连线消失,节点不受影响
序列化点击保存,刷新页面后加载 JSON节点位置、类型、连线关系完全恢复

答辩时,评委最常问的三个问题是:为什么选 jsPlumb 而不是其他方案、连线合法性怎么校验、数据怎么持久化。第一个问题用第 2 章的选型对比表回答,第二个问题现场演示「开始节点不能有入边」的校验逻辑,第三个问题展示 JSON 结构并说明可以存到后端数据库。

我自己的习惯是,每次交付前都会把上面这张表跑一遍,尤其是序列化恢复这一项,因为它是整个方案里最容易出问题的地方。有一次我自信满满地演示,结果恢复后连线全跑到画布外面了,原因就是忘了在loadFlow里调用instance.repaintEverything()。从那以后我每次保存和加载都强制走一遍完整流程,确认连线位置和保存前一致才敢提交。希望这套方案能帮到你,少走一些我踩过的弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询