尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

jsPlumb流程图持久化实战:状态管理与JSON快照

jsPlumb流程图持久化实战:状态管理与JSON快照 简介本资源是一份面向前端开发者与可视化应用实践者的 jsPlumb 流程图开发实战包聚焦于流程图初始化、节点动态增删改、连接线交互控制及状态持久化等核心能力适用于工作流引擎、业务流程建模、网络拓扑可视化等实际场景。压缩包共11个文件230KB含7个JavaScript文件含jsPlumb主库、jQuery依赖、UUID生成器及自定义配置与业务逻辑脚本、2个CSS样式文件负责界面布局与节点视觉呈现、1个HTML入口页及1个GIF动图用于加载提示或交互反馈结构精简、开箱即用。已有1764人学习下载资源完整覆盖从环境搭建到数据保存的全流程代码实现包含可直接运行的页面示例、端点与连接的事件监听机制、JSON序列化/反序列化方案以及群组管理、自定义连接类型等进阶特性实践是掌握jsPlumb工程化落地的高价值参考样本。1. 为什么你写的 jsPlumb 流程图一刷新就“失联”——从静态 HTML 到可持久化交互的临界点很多人把index.html里塞进jquery.jsPlumb-1.7.2.js、拖几个div、调两行jsPlumb.connect()就以为流程图做完了。结果页面一刷新节点还在连线全丢拖动节点后位置错乱删掉一个连接另一个端点却还挂着虚线更别说导出 JSON 或还原上次编辑状态。这不是 jsPlumb 不行而是没踩中它的状态管理范式jsPlumb 本身不维护 DOM 元素坐标、不序列化连接关系、不绑定数据模型——它只管“画布上的视觉连接”而真正的业务状态谁连谁、在哪、什么样式必须由开发者显式建模、捕获、还原。本实例不是教你怎么“画线”而是带你构建一个具备完整生命周期控制能力的流程图内核从index.html静态加载开始到动态增删节点/连接、实时响应拖动、按需保存结构化快照、再无损恢复——所有操作都落在jsPlumb-1.7.2的原生 API 范围内不依赖任何服务端或额外框架。适合正在用 jQuery 技术栈开发审批流、拓扑图、低代码表单编排的前端工程师尤其当你发现jsPlumb.getInstance().getConnections()返回空数组、或restoreConnections()报endpoint not found时这篇就是为你写的。2. 初始化与容器准备为什么必须用div而非svg作为根容器jsPlumb 的渲染层抽象了 canvas/SVG/VM L但它的逻辑容器必须是普通 DOM 元素通常是div而非直接操作svg标签。这是因为 jsPlumb 的端点Endpoint和连接Connection本质上是依附于 DOM 元素的“锚点”其位置计算依赖元素的offsetTop/offsetLeft和getBoundingClientRect()而svg内部的g或circle不具备这些布局属性。若强行将jsPlumb.addEndpoint()绑定到 SVG 子元素会导致坐标偏移、拖动失效、连接线断裂。2.1 HTML 结构规范容器、节点、端点三者分离!DOCTYPE html html head meta charsetutf-8 titlejsPlumb 完整实例/title link relstylesheet hrefcss/layui.css link relstylesheet hrefcss/jvisio.css !-- 注意顺序jQuery 必须在 jsPlumb 之前 -- script srcjs/jquery-1.10.2.min.js/script script srcjs/jquery-ui.min.js/script script srcjs/uuid.min.js/script script srcjs/mustache.min.js/script script srcjs/jquery.jsPlumb-1.7.2.js/script /head body !-- 主容器必须是 position: relative 的 div -- div idflowchart-container styleposition: relative; width: 100vw; height: 80vh; background: #f8f9fa;/div !-- 节点模板Mustache 渲染用 -- script idnode-template typetext/template div classjv-node>// 创建全局 jsPlumb 实例注意1.7.2 版本必须显式 new const jsPlumbInstance jsPlumb.getInstance({ // 强制使用 SVG 渲染器Canvas 在高 DPI 屏幕下模糊VML 已淘汰 Renderer: svg, // 禁用自动连接检测避免鼠标悬停误触发 ConnectionsDetachable: false, // 允许拖动节点必须开启否则 dragstart 事件不生效 DragOptions: { cursor: move }, // 连接线样式全局默认 Connector: [Flowchart, { cornerRadius: 5, alwaysRespectStubs: true }], // 端点样式全局默认 Endpoint: [Dot, { radius: 6 }], // 连接线悬停/选中高亮 HoverClass: jv-connection-hover, // 禁用右键菜单防止干扰流程图操作 ConstrainDragToContainer: true }); // 绑定容器使 jsPlumb 知道渲染边界 jsPlumbInstance.setContainer(flowchart-container);参数说明Renderer: svg1.7.2版本中svg是唯一稳定支持缩放、高 DPI 和复杂路径的渲染器canvas在zoom后线条锯齿严重vml仅 IE8 支持且已废弃。ConstrainDragToContainer: true关键若为false节点拖出容器后getOffset()返回负值保存坐标时出现-120px类错误值导致还原失败。ConnectionsDetachable: false关闭默认拖拽断开行为避免用户无意中拉断连接后续通过按钮或右键菜单显式控制断开逻辑。2.3 端点注册与类型预定义避免addEndpoint时重复创建jsPlumb 允许为不同用途的端点定义类型如source、target、error并在connect()时指定。这比每次手动传样式参数更可控// 预注册两种端点类型复用样式配置 jsPlumbInstance.registerConnectionType(default, { connector: [Flowchart, { cornerRadius: 5 }], paintStyle: { stroke: #4a90e2, strokeWidth: 2 }, hoverPaintStyle: { stroke: #1e5799 } }); jsPlumbInstance.registerEndpointType(source, { endpoint: [Dot, { radius: 7 }], paintStyle: { fill: #4a90e2 }, isSource: true, maxConnections: -1 // -1 表示不限制连接数 }); jsPlumbInstance.registerEndpointType(target, { endpoint: [Dot, { radius: 7 }], paintStyle: { fill: #e74c3c }, isTarget: true, maxConnections: -1 });注意registerEndpointType中的isSource/isTarget必须与后续connect()的source/target参数严格匹配否则连接失败且无报错。这是1.7.2版本最隐蔽的坑之一。3. 动态节点与连接管理从 DOM 操作到状态映射的双向同步jsPlumb 的核心矛盾在于它管理的是“视觉连接”而业务需要的是“语义连接”。例如一个审批节点 A 连接到 B业务上表示“A 审批通过后流转至 B”但 jsPlumb 只知道“A 的 source 端点连到了 B 的 target 端点”。因此必须建立一张映射表将 DOM 元素 ID ↔ 业务数据 ID ↔ 连接关系三者绑定。3.1 节点创建与端点注入Mustache 模板 UUID 生成// 使用 uuid.v4() 生成唯一业务 ID非 DOM ID避免重名冲突 function createNode(name, desc, left 100, top 100) { const nodeId uuid.v4(); // 生成分布式唯一 ID兼容多实例部署 const template document.getElementById(node-template).innerHTML; const html Mustache.render(template, { id: nodeId, name, desc, left, top }); // 插入 DOM $(#flowchart-container).append(html); // 获取新节点 DOM 元素 const $node $([data-id${nodeId}]); // 为 source 和 target 端点分别添加 jsPlumb 端点 jsPlumbInstance.addEndpoint($node.find(.jv-source), { endpoint: source, anchor: Right }); jsPlumbInstance.addEndpoint($node.find(.jv-target), { endpoint: target, anchor: Left }); // 将业务 ID 与 DOM 元素绑定供后续查找 $node.data(businessId, nodeId); return nodeId; } // 示例创建两个节点 const startId createNode(开始, 流程起点, 50, 200); const approveId createNode(审批, 部门负责人审批, 300, 200);关键逻辑说明uuid.v4()替代Math.random()确保跨浏览器、多用户场景下节点 ID 全局唯一避免saveState()时因 ID 冲突导致连接丢失。anchor: Right/anchor: Left指定端点吸附位置Right表示端点始终位于元素右侧中心避免拖动时端点漂移。$node.data(businessId, nodeId)使用 jQuery.data()存储业务 ID比dataset更兼容老版 IE且不污染 HTML 属性。3.2 连接创建与双向绑定connect()的参数陷阱与事件监听// 建立连接必须传入 source/target 的 DOM 元素非 selector 字符串 function connectNodes(sourceId, targetId) { const $sourceNode $([data-id${sourceId}]); const $targetNode $([data-id${targetId}]); // 获取 source 端点取第一个 .jv-source 元素 const sourceEndpoint $sourceNode.find(.jv-source)[0]; // 获取 target 端点取第一个 .jv-target 元素 const targetEndpoint $targetNode.find(.jv-target)[0]; // 执行连接注意参数必须是 DOM 元素不能是 jQuery 对象或字符串 const connection jsPlumbInstance.connect({ source: sourceEndpoint, target: targetEndpoint, type: default, // 添加业务元数据用于后续序列化 data: { sourceId: sourceId, targetId: targetId, type: approval } }); // 监听连接删除事件同步清理业务状态 jsPlumbInstance.bind(connectionDetached, function(connInfo) { if (connInfo.connection.data connInfo.connection.data.sourceId) { console.log(连接已断开${connInfo.connection.data.sourceId} → ${connInfo.connection.data.targetId}); // 此处可触发 API 删除数据库记录 } }); return connection; } // 示例连接 connectNodes(startId, approveId);注意jsPlumb.connect()的source/target参数必须是原生 DOM 元素如element传入jQuery对象$el[0]或 CSS 选择器字符串#node1会导致静默失败。这是1.7.2版本文档未明确强调的硬性约束。3.3 节点拖动与坐标持久化监听dragstop而非dragjsPlumb 的drag事件每帧触发高频写入 DOM 会导致卡顿而dragstop仅在拖动结束时触发一次适合更新坐标// 为所有节点绑定拖动事件委托到容器避免重复绑定 $(#flowchart-container).on(mousedown, .jv-node, function(e) { const $node $(this); const nodeId $node.data(businessId); // jsPlumb 自动处理拖动我们只需在停止时保存坐标 jsPlumbInstance.bind(dragStop, function(params) { // params.el 是被拖动的 DOM 元素 if (params.el this) { const offset $node.offset(); const containerOffset $(#flowchart-container).offset(); // 计算相对于容器的坐标关键 const left offset.left - containerOffset.left; const top offset.top - containerOffset.top; // 更新 DOM 样式保持视觉一致 $node.css({ left: left px, top: top px }); // 同步更新业务状态此处存入内存对象实际可存 localStorage nodeState[nodeId] { ...nodeState[nodeId], left, top }; console.log(节点 ${nodeId} 新坐标${left}, ${top}); } }); });坐标计算原理offset()返回元素相对于文档左上角的绝对坐标containerOffset是容器相对于文档的坐标left offset.left - containerOffset.left得到相对于容器的相对坐标这才是saveState()时应保存的值。若直接保存offset.left还原时会因容器尺寸变化导致错位。4. 状态序列化与还原JSON 快照的字段设计与restoreConnections实现jsPlumb-1.7.2没有内置exportJSON()方法必须手动遍历getConnections()和getEndpoints()构建快照。但直接调用jsPlumb.restoreConnections()会失败——因为它要求所有目标端点必须已存在而restoreConnections()不负责重建端点。4.1 状态对象结构设计业务 ID 优先拒绝 DOM ID// 全局状态对象模拟业务数据模型 const flowState { nodes: {}, // key: businessId, value: { id, name, desc, left, top } connections: [] // [{ sourceId, targetId, type }] }; // 保存当前状态到内存实际可存 localStorage 或 POST 到后端 function saveState() { // 1. 保存节点坐标和基础信息 $(.jv-node).each(function() { const $node $(this); const nodeId $node.data(businessId); const offset $node.offset(); const containerOffset $(#flowchart-container).offset(); flowState.nodes[nodeId] { id: nodeId, name: $node.find(.jv-node-header).text(), desc: $node.find(.jv-node-body).text(), left: offset.left - containerOffset.left, top: offset.top - containerOffset.top }; }); // 2. 保存连接关系只存业务 ID不存 DOM 元素 const connections jsPlumbInstance.getConnections(); flowState.connections connections.map(conn ({ sourceId: conn.data.sourceId, targetId: conn.data.targetId, type: conn.data.type || default })); console.log(已保存状态, JSON.stringify(flowState, null, 2)); return JSON.stringify(flowState); } // 示例调用 $(#save-btn).click(saveState);字段设计理由nodes使用businessId为 key避免因 DOM ID 变更如页面重载导致映射断裂connections中sourceId/targetId与nodes的 key 一致保证还原时能精准查找到对应节点不保存paintStyle等样式字段样式应由 CSS 控制状态只存语义数据。4.2 状态还原分两步——先建节点再连连接// 从 JSON 字符串还原流程图 function restoreState(jsonStr) { try { const state JSON.parse(jsonStr); // 步骤1清空现有节点和连接谨慎 jsPlumbInstance.removeAllEndpoints(); $(.jv-node).remove(); // 步骤2重建所有节点按坐标插入 Object.values(state.nodes).forEach(nodeData { // 使用 Mustache 渲染节点复用 createNode 逻辑但跳过 UUID 生成 const template document.getElementById(node-template).innerHTML; const html Mustache.render(template, { id: nodeData.id, name: nodeData.name, desc: nodeData.desc, left: nodeData.left, top: nodeData.top }); $(#flowchart-container).append(html); const $node $([data-id${nodeData.id}]); $node.data(businessId, nodeData.id); // 重新添加端点必须在节点 DOM 存在后执行 jsPlumbInstance.addEndpoint($node.find(.jv-source), { endpoint: source, anchor: Right }); jsPlumbInstance.addEndpoint($node.find(.jv-target), { endpoint: target, anchor: Left }); }); // 步骤3重建连接此时所有端点已存在 state.connections.forEach(conn { const $sourceNode $([data-id${conn.sourceId}]); const $targetNode $([data-id${conn.targetId}]); if ($sourceNode.length $targetNode.length) { const sourceEl $sourceNode.find(.jv-source)[0]; const targetEl $targetNode.find(.jv-target)[0]; jsPlumbInstance.connect({ source: sourceEl, target: targetEl, type: conn.type }); } }); console.log(状态还原成功); } catch (e) { console.error(还原失败, e); } } // 示例从 localStorage 加载 $(#load-btn).click(() { const saved localStorage.getItem(jvisio-state); if (saved) restoreState(saved); });关键点restoreState()必须严格遵循「先节点、后连接」顺序。jsPlumb.restoreConnections()在1.7.2中不可靠因其内部依赖getElementById查找端点而我们的端点不在id属性中。手动connect()是唯一稳定方案。5. 进阶技巧连接线标签、条件分支与防误操作保护真实流程图常需在连接线上显示文字如“是”、“否”、支持条件分支一个 source 连多个 target、以及防止用户误操作如禁止自连、限制最大连接数。这些功能无需修改 jsPlumb 源码仅靠 API 组合即可实现。5.1 连接线添加标签Overlay的正确用法// 为连接添加文本标签Overlay function addLabelToConnection(connection, text) { connection.addOverlay([ Label, { label: text, location: 0.5, // 位置0.5 表示中点 cssClass: jv-connection-label, id: label-overlay } ]); } // 创建连接后立即加标签 const conn connectNodes(startId, approveId); addLabelToConnection(conn, 提交申请);CSS 样式jvisio.css.jv-connection-label { background: white; padding: 2px 6px; border-radius: 3px; font-size: 12px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); color: #333; }注意location: 0.5是比例值0起点1终点非像素值id用于后续removeOverlay(label-overlay)动态更新。5.2 条件分支实现一个 source 连接多个 target 的安全模式// 安全连接函数检查是否已存在相同 source→target 连接避免重复 function safeConnect(sourceId, targetId, label) { const $sourceNode $([data-id${sourceId}]); const $targetNode $([data-id${targetId}]); // 检查是否已存在该连接防止重复点击 const existing jsPlumbInstance.getConnections({ source: $sourceNode.find(.jv-source)[0], target: $targetNode.find(.jv-target)[0] }); if (existing.length 0) { console.warn(连接 ${sourceId} → ${targetId} 已存在); return existing[0]; } const conn jsPlumbInstance.connect({ source: $sourceNode.find(.jv-source)[0], target: $targetNode.find(.jv-target)[0], type: default }); if (label) addLabelToConnection(conn, label); return conn; } // 示例审批节点分叉为“通过”和“驳回” const approveId createNode(审批, 负责人审批, 300, 200); const passId createNode(通过, 进入下一环节, 550, 150); const rejectId createNode(驳回, 退回申请人, 550, 250); safeConnect(approveId, passId, 是); safeConnect(approveId, rejectId, 否);5.3 防误操作保护禁用自连与连接数限制// 全局连接验证钩子在 connect 前拦截 jsPlumbInstance.bind(beforeDrop, function(info) { const sourceId $(info.source).closest(.jv-node).data(businessId); const targetId $(info.target).closest(.jv-node).data(businessId); // 禁止自连 if (sourceId targetId) { alert(不允许连接到自身); return false; // 阻止连接 } // 限制每个 target 最多接收 3 个连接如审批节点最多被 3 个前置节点触发 const targetNode $(info.target).closest(.jv-node); const incomingCount jsPlumbInstance.getConnections({ target: targetNode.find(.jv-target)[0] }).length; if (incomingCount 3) { alert(节点 ${targetId} 已达到最大输入连接数3); return false; } return true; // 允许连接 });提示beforeDrop是1.7.2中最有效的连接拦截点它在用户松开鼠标、jsPlumb 准备创建连接前触发返回false可彻底阻止连接生成比connection事件后删除更高效。jsPlumb-1.7.2的稳定性来自对 DOM 生命周期的敬畏——它不替你管理数据但给你足够细的钩子去掌控每一根线的生与死。当你把saveState()的 JSON 保存到localStorage再用restoreState()在页面加载时重建那个“一刷新就失联”的流程图就真正成了你应用里的一个可序列化、可版本化、可协作的业务实体。本文还有配套的精品资源点击获取
返回列表