老项目流程设计器实战:jsPlumb+jQuery实现拖拽连线与保存还原
2026/9/8 6:32:09 网站建设 项目流程

简介:一份基于jsPlumb与jQuery的流程图设计实例资源,面向Web前端开发者、系统设计人员,也适合需要快速搭建业务流程、系统架构或工作流图的初学者。压缩包共216个文件,以png截图、js脚本、gif演示、css样式与html示例为主,配合cs/aspx等配置文件展示完整的前后端项目结构,包体约984KB,便于按需查阅与二次开发。资源已有721人学习下载,内容覆盖jsPlumb从基础初始化、addEndpoint端点创建、connect连接建立到事件监听与样式扩展的完整流程;借助flowchartDemo-master项目,读者可拿到可直接运行的示例代码、分模块视觉参考图和动态交互效果。上百张截图可以对照源码定位每一步操作,gif动画直观展示连线过程,适合理解Bezier连线、拖拽调整、自定义样式及数据绑定等关键知识点,作为教学演示或项目起步模板都很实用。 去年接手公司内部审批管理平台的改造,产品扔过来的需求一句话:做一个流程设计器,让管理员自己在页面上把审批节点拖出来、连上线、保存好,下次打开还能看到。我看了一眼代码仓库,整站还是 jQuery 时代的 jsp 结构,引入 Vue 或 React 都不现实。最后用了 jsPlumb+jQuery 这套组合,两周内把流程设计器从零做到了可交付。这篇文章把项目落地过程中的实现思路、核心代码和排坑记录完整整理出来,给正在评估同类方案的人做参考。

如果你也是在一个老前端项目里做流程配置、节点编排这类功能,或者需要一份能直接抄作业的 jsPlumb 实战示例,这篇可以直接照着走。我不打算从零讲 JavaScript 基础,但会把 jsPlumb 的关键概念、初始化细节、拖拽连线、保存还原、动态节点这些问题全部过一遍,都是生产环境里真实验证过的写法。

1. 先说说为什么还在用这套老牌组合

1.1 这套技术选型适合什么项目

现在前端圈子默认都在讲 Vue、React,但存量业务里有大量老后台管理系统还是 jQuery 技术栈。为了一个流程设计器去重构整个前端,成本太高,风险也大,这在大部分公司都是不会被批准的方案。jsPlumb 2.x 从这个角度来说非常友好:它本身就是从那一代前端生态里长起来的库,与 jQuery 搭配使用几乎没有额外学习成本,API 风格也贴近老前端的使用习惯。

选型时我也对比过 GoJS、Drawflow、LogicFlow 这些同类库。GoJS 功能确实最强,但商用要买授权;Drawflow 很轻,可项目更新节奏不稳定,遇到问题社区能查到的东西不多;LogicFlow 设计理念好,可它在 Vue 生态里更顺手,硬塞到 jQuery 老项目里接入成本略高。jsPlumb 2.x 的优势在于:对 jQuery 友好、API 成熟、社区十几年积攒下来的问题答案非常全,遇到奇怪问题基本都能搜到解决方案,这在工期紧张的项目里是实打实的救命优势。

我不鼓吹“老技术永远最好”。如果从零搭一个全新系统,用新框架的新库体验确实更好。但如果你面对的是一个还在维护的 jQuery 老项目,jsPlumb+jQuery 就是性价比最高的增量开发方案。这也是我把它单独写出来的原因。

1.2 jsPlumb 的四个核心概念一次讲明白

初次接触 jsPlumb,会被 Endpoint、Anchor、Connector、Overlay 这些术语劝退。我建议你用生活场景去理解:

概念作用生活类比
Anchor连接线在节点上“吸附”的位置插座装在哪面墙上
Endpoint实际渲染出来、用户可拖拽的圆点插座实体
Connector两个端点之间的连线样式插头接好后的电线
Overlay连接线上的装饰物,箭头、文字等电线上挂的标签牌

jsPlumb 底层用的是 SVG 渲染,节点位置变化时它会重算所有连接线的 path 路径。正是这个特性让它天然适合做流程设计器:节点拖到哪,线就跟到哪。搞懂这四个概念,剩下的就是 set、get、trigger 的常规操作。

2. 初始化与画布搭建:先把架子支起来

2.1 依赖引入和页面结构

先说明版本搭配:jquery 3.5.1、jquery-ui 1.12.1、jsPlumb 2.15.6。这个组合是同事踩过一轮坑后固定下来的,jQuery UI 负责拖拽,jsPlumb 负责画布连线,两者各管一摊,互不抢活。

页面结构采用经典三段式:左侧组件托盘、中间画布、底部或右侧属性栏。第一个版本里没做属性栏,只保留托盘和画布也能跑通整个流程。

<div class="designer-wrap"> <div class="palette"> <h4>组件库</h4> <div class="palette-item">var jpb = jsPlumb.getInstance({ Container: 'flowCanvas', DragOptions: { cursor: 'pointer', zIndex: 2000 }, Connector: ['Bezier', { curviness: 60 }], PaintStyle: { stroke: '#409EFF', strokeWidth: 2 }, Endpoint: ['Dot', { radius: 6 }], EndpointStyle: { fill: '#409EFF', outlineStroke: '#fff', outlineWidth: 1 }, ConnectionOverlays: [ ['Arrow', { location: 1, width: 12, length: 12 }], ['Label', { location: 0.5, cssClass: 'edge-label' }] ] });

这里的curviness参数控制贝塞尔曲线的弯曲程度,内部系统里 60 比较顺眼,再大会绕得很夸张。如果你的场景是上下节点排列、视觉上要非常严谨的流程图,把 Connector 换成['Flowchart'],出来就是直角折线,更接近传统流程图的观感。

初始化时还有一层容易忽略的逻辑:连线过程中如果用户没对准目标端点就松手,jsPlumb 默认不会创建连接,这是它的内置行为。但为了避免鼠标在节点内部空白区域误触发连线,我会在后续给每个端点加上filter属性,只允许从端点的圆点上开始拖。

3. 节点拖拽、连接线与交互细节

3.1 从组件库拖出新节点

这个交互分成两段:托盘里的元素通过 jQuery UI 的draggable配合helper: 'clone'克隆一份;克隆体放到画布上方后,转成画布内坐标系的位置并落地。

$('.palette-item').draggable({ helper: 'clone', containment: 'body', stop: function (e, ui) { var type = $(this).data('type'); var $node = $('<div>') .addClass('flow-node') .attr('id', 'node_' + Date.now() + '_' + Math.floor(Math.random() * 1000)) .data('type', type) .css({ left: ui.position.left - $('.canvas').offset().left, top: ui.position.top - $('.canvas').offset().top, position: 'absolute' }) .text(getNodeText(type)) .appendTo('.canvas'); initNode($node); } });

计算 left/top 时,我直接减去画布容器的 offset,避免托盘和画布不在同一层级时出现落点偏移。如果你不管这个偏移,实际测试就会发现节点总落在鼠标右下角一段距离的位置,非常别扭。

initNode负责给节点注册 jsPlumb 端点和 jQuery draggable:

function initNode($el) { var id = $el.attr('id'); $el.draggable({ containment: 'flowCanvas', grid: [10, 10], stop: function () { jpb.repaintEverything(); } }); jpb.addEndpoint(id, { isSource: true, anchor: ['Right', 'Left', 'Top', 'Bottom'] }); jpb.addEndpoint(id, { isTarget: true, anchor: ['Right', 'Left', 'Top', 'Bottom'] }); jpb.draggable(id); }

注意jQuery UI draggablejpb.draggable是两回事。jQuery UI 负责让元素物理移动,jpb.draggable负责在移动过程中让连接线实时跟随。只调 jQuery UI 不加jpb.draggable,拖节点的时候线条不会跟着走,这是新手最容易踩的第一个坑。

3.2 节点上的端点和连接线如何配置

给节点配置四个方向的锚点,意味着用户从任意方向都能找到出口。这在流程图上很实用,不用像在老旧绘图工具里那样非得对准手柄才能连线。

有一个参数特别容易被忽略:maxConnections。它默认是 1,也就是一个源端点只能拉出去一根线。如果节点要作为分叉入口,必须显示设置为 -1:

jpb.addEndpoint(id, { isSource: true, endpoint: ['Dot', { radius: 5 }], maxConnections: -1 }, { anchor: ['Left', 'Right', 'Top', 'Bottom'] });

很多人做完发现节点只能连一根线,就是忘记改这个参数。

连线创建我走的是事件驱动的路径。用户从源端点拖动到目标端点上,内部会触发connection事件,在这里统一给新线加默认文案:

jpb.bind('connection', function (info) { var overlay = info.connection.getOverlay('edge-label'); overlay.setLabel('未命名'); });

双击一条连接线可以改名,做法是给 label 覆盖物绑定dblclick,在事件里遍历所有连接,找到对应的 Overlay 实例再更新文本。这条交互不是 jsPlumb 内置能力,需要自己补齐,但对流程配置类系统来说很实用,能让用户直接在图上标注“同意”“驳回”之类的分支说明。

3.3 右键菜单和删除操作的实现思路

jsPlumb 没有内置右键菜单,我用contextmenu事件自绘一个轻量菜单。判断目标是节点还是连接线,走两套删除逻辑。

删除节点时的关键点在于:不能只调 jQuery 的 remove。先调用jpb.removeAllEndpoints(nodeId)清理节点上挂的所有端点和连接,再移除 DOM,否则画面上节点没了,但连接线还残留在内存和画布里。

function removeNode(nodeId) { jpb.removeAllEndpoints(nodeId); $('#' + nodeId).remove(); }

删除连接线稍微绕一点。jsPlumb 渲染的连接线 DOM 上有.jtk-connector类,右键时可遍历jpb.getConnections(),找到渲染元素包含当前事件目标的那条连接,再调jpb.deleteConnection(connection)。删除后也要顺手清理相关 overlay 的 DOM,避免残留小块元素。

4. 保存和还原:把流程图变成 JSON

4.1 序列化节点与连接关系

流程设计器最核心的能力是“画完能存,存了能打开”。序列化逻辑不复杂,遍历所有节点和连接,导出为 JSON 对象。

function serializeGraph() { var nodeList = [], connList = []; $('.flow-node').each(function () { var $n = $(this); var pos = $n.position(); nodeList.push({ id: $n.attr('id'), type: $n.data('type'), x: pos.left, y: pos.top, label: $n.text() }); }); $.each(jpb.getConnections(), function (i, conn) { connList.push({ sourceId: conn.sourceId, targetId: conn.targetId, label: conn.getOverlay('edge-label').getLabel() }); }); return { nodes: nodeList, connections: connList }; }

在后端对接时,这个对象整体转成 JSON 字符串存数据库一个字段就可以。节点坐标、节点类型、显示文案、连线两端 id 和线标签,这几项信息对一张流程图来说已经足够完整。如果后续要支持节点颜色、备注、执行人配置,在节点对象里继续扩展字段即可。

4.2 从 JSON 还原完整画布

还原时最重要的顺序:先全部建节点,再全部连线。如果边建节点边连线,很容易出现目标节点还没创建完、连接线找不到落点的情况,尤其后端返回的数据顺序不固定时,这个问题会被无限放大。

function restoreGraph(data) { jpb.reset(); $('.flow-node').remove(); $.each(data.nodes, function (i, item) { var $node = createNode(item); initNode($node); }); $.each(data.connections, function (i, item) { jpb.connect({ source: item.sourceId, target: item.targetId, overlays: [ ['Label', { label: item.label || '', id: 'edge-label' }] ] }); }); jpb.repaintEverything(); }

jpb.reset()会清掉实例内部的所有元素引用,但不会自动清 DOM,所以要手动 remove 掉画布里的旧节点再重建。还原结束后调一次repaintEverything,把所有连接线重绘一遍,防止因为节点位置计算时机太早导致线条偏移。

4.3 唯一 ID:整个数据模型的地基

整个流程设计器里我反复强调 id,因为 jsPlumb 内部管理节点、连接全部靠元素 id 作为 key。如果两个节点 id 相同,jsPlumb 会把第二个节点识别成第一个,连线直接错乱。

创建节点时用“当前时间戳 + 随机数”组合,能覆盖大部分场景。实际项目中还可以加上用户 id、页面标识做前缀,进一步降低冲突概率。这听起来是小细节,但我见过生产环境因为一个重复 id,流程图数据显示成“所有节点连到同一个节点”的诡异 bug,排查了整整一天。

还原节点时还要注意:如果数据库里存的 id 和当前页面里其他元素的 id 撞了,刷新后编辑器状态会乱。稳妥做法是序列化时使用内部唯一 id,显示时再映射成业务名称,两者分开,不要让用户看到的展示内容直接参与内部状态管理。

5. 生产中常见的坑和我的处理办法

5.1 节点拖拽后连线纹丝不动的问题

这是个高频现象:节点能拖,线能连,但一拖节点,连接线端点留在原地不动,看起来像断了一样。

根因在于 jsPlumb 没有接管拖动。我在前面已经强调过,Draggable 要同时用 jQuery UI 的draggablejpb.draggable,让 jsPlumb 知道“这个元素在移动,需要重算连接线位置”。

如果确实用了jpb.draggable还是不跟随,排查两件事:一是看 draggable 初始化是否在addEndpoint之前,顺序反了会导致端点事件被覆盖;二是拖拽过程中是否有人为调用了全局 repaint,导致内部状态被重置。我的经验是:先addEndpoint,再jpb.draggable,最后在 stop 里repaintEverything,这个顺序基本不会出问题。

5.2 动态创建节点时原型的 id 会被复制

用 clone 方式从托盘拖节点时,jQuery 的clone()会把源元素的 id 一起复制。如果你没在克隆后手动改 id,拖出第二个节点时页面上就有两个相同 id 的元素,jsPlumb 直接崩溃或出现诡异状态。

解决方式其实在 3.1 的样例代码里已经体现:创建节点时强制执行新的唯一 id。还有一个隐藏坑值得提醒:托盘里的模板元素最好本身就不要设 id,用>jpb.setZoom(0.8);

调用一次会重新计算所有连接线的坐标。如果项目里已经写了自定义缩放逻辑,缩放后调jpb.repaintEverything()也能得到一个基本一致的视觉效果。更进一步的做法是控制画布内部坐标系,把鼠标位置换算成画布坐标来放置节点,这样缩放后新增节点不会错位。我在项目里用容器整体缩放配合repaintEverything,测试下来相对稳定。如果你的流程设计器交互非常复杂,建议从一开始就做坐标转换,不要依赖缩放 API。

5.4 更多问题速查

问题原因处理办法
节点添加不上端点容器还没渲染完成就 addEndpoint放到$(document).ready或请求回调里执行
弹窗里画布偏移弹窗动画滚动导致 offset 变化初始化前重新取画布位置,或延迟初始化
多条连线叠成一条端点 anchor 全在同一位置给不同源端点配置不同 anchor 方向
连线标签中文乱码页面编码或引入字体问题检查 meta charset,并确认后端存储用 UTF-8
删除节点后删不了线只删 DOM 没有清理 jsPlumb 状态removeAllEndpoints清理关联连接

5.5 许可证问题值得提前确认

这块纯属经验提醒。jsPlumb 的 2.x 版本是 MIT 开源可商用,但 3.x 之后的默认版本涉及商业授权。很多人在网上看到新版本教程就直接引入,没注意到许可证已经变更。如果是公司商用项目,建议锁定 2.x 版本,或者统一走付费授权流程,避免法务风险。这个点我在项目做完技术复盘时才注意到,幸好当时用的是 2.x,没有出问题。

写到这里,这个流程设计器已经能完成从组件库拖节点、连接线、保存 JSON、还原画布这一整条流程,生产环境里主要的坑也做了交代。如果你要在老 jQuery 项目里做类似功能,按这个思路搭是可行的。整个实现最核心的一句话:让 jsPlumb 统一管理节点和连接的状态,jQuery UI 只负责视觉层拖拽,两者职责分开,问题就少一半。另外建议在做项目管理页时顺手做一个流程图版本号字段,后续业务方改流程时能回溯历史版本,这个扩展我当时没做,后面补的时候费了不少劲。

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

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

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

立即咨询