ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

老项目流程设计器实战:jsPlumb+jQuery实现拖拽连线与保存还原

老项目流程设计器实战:jsPlumb+jQuery实现拖拽连线与保存还原 简介一份基于jsPlumb与jQuery的流程图设计实例资源面向Web前端开发者、系统设计人员也适合需要快速搭建业务流程、系统架构或工作流图的初学者。压缩包共216个文件以png截图、js脚本、gif演示、css样式与html示例为主配合cs/aspx等配置文件展示完整的前后端项目结构包体约984KB便于按需查阅与二次开发。资源已有721人学习下载内容覆盖jsPlumb从基础初始化、addEndpoint端点创建、connect连接建立到事件监听与样式扩展的完整流程借助flowchartDemo-master项目读者可拿到可直接运行的示例代码、分模块视觉参考图和动态交互效果。上百张截图可以对照源码定位每一步操作gif动画直观展示连线过程适合理解Bezier连线、拖拽调整、自定义样式及数据绑定等关键知识点作为教学演示或项目起步模板都很实用。 去年接手公司内部审批管理平台的改造产品扔过来的需求一句话做一个流程设计器让管理员自己在页面上把审批节点拖出来、连上线、保存好下次打开还能看到。我看了一眼代码仓库整站还是 jQuery 时代的 jsp 结构引入 Vue 或 React 都不现实。最后用了 jsPlumbjQuery 这套组合两周内把流程设计器从零做到了可交付。这篇文章把项目落地过程中的实现思路、核心代码和排坑记录完整整理出来给正在评估同类方案的人做参考。如果你也是在一个老前端项目里做流程配置、节点编排这类功能或者需要一份能直接抄作业的 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 老项目jsPlumbjQuery 就是性价比最高的增量开发方案。这也是我把它单独写出来的原因。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 classdesigner-wrap div classpalette h4组件库/h4 div classpalette-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 draggablefunction 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 draggable和jpb.draggable是两回事。jQuery UI 负责让元素物理移动jpb.draggable负责在移动过程中让连接线实时跟随。只调 jQuery UI 不加jpb.draggable拖节点的时候线条不会跟着走这是新手最容易踩的第一个坑。3.2 节点上的端点和连接线如何配置给节点配置四个方向的锚点意味着用户从任意方向都能找到出口。这在流程图上很实用不用像在老旧绘图工具里那样非得对准手柄才能连线。有一个参数特别容易被忽略maxConnections。它默认是 1也就是一个源端点只能拉出去一根线。如果节点要作为分叉入口必须显示设置为 -1jpb.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. 保存和还原把流程图变成 JSON4.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 的draggable和jpb.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 只负责视觉层拖拽两者职责分开问题就少一半。另外建议在做项目管理页时顺手做一个流程图版本号字段后续业务方改流程时能回溯历史版本这个扩展我当时没做后面补的时候费了不少劲。本文还有配套的精品资源点击获取
返回列表