LogicFlow节点连线规则实战:如何用sourceRules和targetRules实现精准控制

📅 发布时间:2026/7/9 1:37:35 👁️ 浏览次数:
LogicFlow节点连线规则实战:如何用sourceRules和targetRules实现精准控制
LogicFlow节点连线规则实战如何用sourceRules和targetRules实现精准控制最近在重构一个内部流程设计器时我遇到了一个典型问题用户总是能随意连接节点导致生成大量无效甚至错误的流程图。比如把“结束”节点连到“开始”节点或者在审批节点后面直接接上另一个审批节点。这种自由度过高的设计表面上给了用户灵活性实际上却成了数据混乱和逻辑错误的源头。对于需要严谨流程的业务场景——比如工单流转、BPMN建模或者低代码平台的逻辑编排——我们必须对节点间的连接施加精确的控制。LogicFlow作为一款专注于流程图绘制的开源库其强大之处不仅在于基础的绘制能力更在于它提供了一套声明式、可编程的规则引擎允许开发者深入到“连接”这一原子操作层面进行干预。sourceRules和targetRules正是这套引擎的核心。它们不是简单的“允许”或“禁止”开关而是可以承载复杂业务逻辑的校验函数。掌握它们意味着你能将业务规则无缝嵌入到可视化交互中让画布本身成为一道坚固的校验防线。这篇文章我将抛开官方文档的平铺直叙结合几个真实的项目案例带你深入理解如何运用sourceRules和targetRules以及与之紧密相关的锚点Anchor定制来构建一个既智能又健壮的流程设计器。我们会从基础规则实现讲到基于锚点的精细化控制最后探讨如何结合状态反馈提升用户体验。无论你是在构建OA审批流、状态机设计器还是复杂的业务编排工具这里的内容都能提供直接的参考。1. 理解规则引擎从“能不能连”到“按什么规则连”在LogicFlow的世界里一条边Edge的诞生始于鼠标在源节点上的按下终于在目标节点上的松开。在这两个关键时刻sourceRules和targetRules分别扮演着守门员的角色。sourceRules当连线操作从一个节点开始时触发。它校验的是“我源节点允许连接到哪些节点”。targetRules当连线试图连接到一个节点时触发。它校验的是“我目标节点允许接受来自哪些节点的连接”。两者的校验是且的关系。一条连线要最终建立必须同时通过源节点的sourceRules和目标节点的targetRules。这种设计将控制权分散到各个节点模型非常符合面向对象的设计思想也让规则的管理和维护变得清晰。1.1 基础规则实现让开始节点只能出不能进让我们从一个最简单的场景开始一个流程必须有且仅有一个开始节点并且这个开始节点不能被任何其他节点连接即只有出边没有入边。首先我们需要自定义一个开始节点。通常我们会继承CircleNode或RectNode但核心逻辑在对应的Model类中。import { CircleNode, CircleNodeModel } from logicflow/core; // 视图部分这里我们简单使用内置圆形 class StartNodeView extends CircleNode {} // 模型部分在这里定义规则 class StartNodeModel extends CircleNodeModel { initNodeData(data) { super.initNodeData(data); // 设置节点基础属性 this.text data.text || 开始; this.width 60; this.height 60; this.style.fill #FF6B6B; // 用红色区分 // 定义作为目标节点时的规则禁止任何连接进入 const noIncomingRule { message: 开始节点不能作为目标节点不能有入边, validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) { // 此规则在StartNode作为target时执行直接返回false表示禁止连接 return false; }, }; this.targetRules.push(noIncomingRule); // 定义作为源节点时的规则可以连接到任何类型节点或按需限制 const outgoingRule { message: 开始节点只能连接到任务或网关节点, validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) { const allowedTypes [task-node, gateway-node]; return allowedTypes.includes(targetNode.type); }, }; this.sourceRules.push(outgoingRule); } } // 导出节点配置 export default { type: start-node, // 节点类型标识符 view: StartNodeView, model: StartNodeModel, };在上面的代码中validate函数是关键。它接收四个参数提供了丰富的上下文信息参数类型描述sourceNodeBaseNodeModel连线起始的节点模型实例targetNodeBaseNodeModel连线目标的节点模型实例sourceAnchorAnchorConfig起始节点上被点击的锚点对象targetAnchorAnchorConfig目标节点上悬停的锚点对象提示validate函数应保持纯净避免副作用。它的唯一职责是根据输入参数返回一个布尔值。复杂的业务逻辑判断如调用API应在此函数外部处理并将结果以节点属性或外部状态的形式提供给validate函数使用。1.2 组合规则实现复杂的流程拓扑约束单一规则往往不够。例如在一个审批流中我们可能要求“会签网关”后面必须接“用户任务”而“用户任务”后面只能接“排他网关”或“结束事件”。这就需要为不同类型的节点配置不同的规则集。假设我们有三种节点user-task用户任务parallel-gateway并行网关exclusive-gateway排他网关。规则如下parallel-gateway的出口只能连接user-task。user-task的出口只能连接exclusive-gateway。exclusive-gateway的出口可以连接user-task或end-event。我们可以为每种节点定义其Model// ParallelGatewayModel 示例 class ParallelGatewayModel extends PolygonNodeModel { initNodeData(data) { super.initNodeData(data); // ... 样式设置 this.sourceRules.push({ message: 并行网关后必须为用户任务, validate: (source, target) target.type user-task }); // 并行网关允许被任何节点连接如果需要限制可添加targetRules } } // UserTaskModel 示例 class UserTaskModel extends RectNodeModel { initNodeData(data) { super.initNodeData(data); // ... 样式设置 this.sourceRules.push({ message: 用户任务后必须为排他网关, validate: (source, target) target.type exclusive-gateway }); this.targetRules.push({ message: 用户任务前必须为并行网关或排他网关, validate: (source, target) [parallel-gateway, exclusive-gateway].includes(source.type) }); } }通过为每个节点类型精确配置sourceRules和targetRules我们就在画布层面构建了一个强类型的流程语法。用户无法进行非法连接从源头保证了流程图的正确性。2. 超越节点类型利用锚点实现端口级控制只控制节点类型的连接有时粒度太粗。比如一个数据库表节点我们可能希望它的每个字段对应一个锚点只能连接到另一个表的特定字段上。这时就需要用到锚点Anchor和更精细的规则校验。LogicFlow允许我们完全自定义节点的锚点位置、数量、外观和属性。结合sourceAnchor和targetAnchor参数我们可以实现端口到端口的连接控制。2.1 自定义锚点与属性绑定让我们创建一个简单的“输入/输出”节点它左侧有一排输入锚点右侧有一排输出锚点并且规则是只能从输出锚点连出只能连接到输入锚点。import { RectNode, RectNodeModel } from logicflow/core; class IoNodeView extends RectNode {} class IoNodeModel extends RectNodeModel { setAttributes() { this.width 120; this.height 80; this.style.fill #E3F2FD; this.style.stroke #2196F3; // 规则1作为源节点时只允许从类型为output的锚点连出 this.sourceRules.push({ message: 只能从右侧输出端口连线, validate: (source, target, sourceAnchor) { return sourceAnchor sourceAnchor.type output; } }); // 规则2作为目标节点时只允许连接到类型为input的锚点 this.targetRules.push({ message: 只能连接到左侧输入端口, validate: (source, target, sourceAnchor, targetAnchor) { return targetAnchor targetAnchor.type input; } }); } // 重写此方法以定义自定义锚点 getDefaultAnchor() { const { x, y, width, height } this; const anchors []; // 左侧三个输入锚点 for (let i 0; i 3; i) { anchors.push({ x: x - width / 2, // 左侧边 y: y - height / 2 (i 1) * (height / 4), // 垂直均匀分布 id: input_${i}, type: input // 自定义属性 }); } // 右侧三个输出锚点 for (let i 0; i 3; i) { anchors.push({ x: x width / 2, // 右侧边 y: y - height / 2 (i 1) * (height / 4), id: output_${i}, type: output // 自定义属性 }); } return anchors; } }在这个例子中我们通过在getDefaultAnchor返回的锚点对象上添加自定义的type属性input/output然后在validate函数中通过sourceAnchor.type和targetAnchor.type来访问它。这样规则就从节点级别细化到了端口级别。2.2 动态锚点与上下文感知更高级的场景是锚点本身需要根据节点状态或属性动态显示或隐藏。例如一个“条件分支”节点只有在满足某些条件时才显示额外的输出端口。LogicFlow的节点模型提供了isSelected、isHovered等状态以及自定义的properties。我们可以在getDefaultAnchor方法中根据这些状态动态计算锚点数组。class ConditionalNodeModel extends RectNodeModel { getDefaultAnchor() { const anchors []; const { x, y, width, height, properties } this; // 始终有一个默认输出 anchors.push({ x: x width / 2, y: y, id: default_output, type: output }); // 仅当节点属性中标记了需要“异常出口”时才显示第二个输出锚点 if (properties.hasExceptionExit) { anchors.push({ x: x width / 2, y: y height / 4, id: exception_output, type: exception_output }); } return anchors; } // 当属性变化时可能需要更新锚点触发重绘 updateAttributes(attr) { super.updateAttributes(attr); // 强制刷新锚点某些版本可能需要调用特定方法或触发事件 this.forceRefreshAnchor(); } }注意动态改变锚点后需要确保LogicFlow能正确重绘节点和相关的边。有时需要手动调用lf.graphModel.refresh或对节点实例进行更新操作。同时要处理好已有连线在锚点消失时的行为是断开还是保持。3. 提供可视化反馈从静默拒绝到明确指引仅仅在连接失败时在控制台抛出一个错误或者静默地阻止连接对用户来说体验是不够友好的。好的设计器应该能提供即时的、可视化的反馈告诉用户“为什么不能连”。3.1 监听连接拒绝事件LogicFlow在连接被规则拒绝时会触发connection:not-allowed事件。这是我们向用户提供反馈的第一个入口点。// 在初始化LogicFlow实例后 lf.on(connection:not-allowed, ({ data, msg }) { // data 包含更详细的错误信息如 sourceNodeId, targetNodeId // msg 是我们在规则中定义的 message 字符串 console.warn(连接被禁止:, msg); // 方式1使用浏览器原生提示简单粗暴可能干扰体验 // alert(无法连接: ${msg}); // 方式2在UI上显示一个自定义的Toast或提示框推荐 showToast(操作受限: ${msg}, warning); // 方式3高亮相关的源节点或目标节点 // lf.graphModel.getNodeModelById(data.sourceNodeId).setState(not-allowed, true); });3.2 利用节点状态进行样式反馈除了弹出提示我们还可以直接改变节点的视觉状态。LogicFlow节点模型有一个state属性当连接校验失败时相关的节点源节点或目标节点的state会被设置为5NODE_STATE.NOT_ALLOWED。我们可以重写getNodeStyle方法来响应这个状态。class FeedbackNodeModel extends RectNodeModel { getNodeStyle() { const style super.getNodeStyle(); const baseStyle { fill: #FFFFFF, stroke: #333333, strokeWidth: 1, }; // 合并基础样式 Object.assign(style, baseStyle); // 根据状态覆盖样式 if (this.state 5) { // 连接不被允许的状态 style.stroke #FF4757; // 红色边框 style.strokeWidth 3; style.strokeDasharray 5,5; // 虚线边框 style.fill #FFE5E7; // 浅红色填充 } else if (this.isHovered) { style.stroke #1E90FF; style.strokeWidth 2; } else if (this.isSelected) { style.stroke #FFA502; style.strokeWidth 2; } return style; } }这样当用户试图进行一个非法连接时相关的节点会立即呈现错误状态比如红色虚线高亮松开鼠标后恢复。这种即时反馈比弹窗更直观干扰也更小。3.3 预验证与悬停反馈更进一步我们可以在鼠标拖拽连线、悬停在潜在目标节点上时就进行预验证并给出反馈。这需要结合mouseover事件和规则的提前校验。一种实现思路是监听边的拖拽事件在targetNode变化时手动调用规则校验函数并提前更新节点状态。let currentDragEdge null; lf.on(edge:add, ({ data }) { // 开始创建边时记录当前边信息可能需要自定义边视图来捕获更早的事件 }); // 监听画布鼠标移动计算当前悬停的节点 lf.on(blank:mousemove, ({ e }) { if (!currentDragEdge) return; const point lf.getPointByClient(e.clientX, e.clientY); const node lf.getNodeModelById(lf.getNodeInPoint(point)); // 伪代码需根据坐标查找节点 if (node node ! lastHoveredNode) { // 对 lastHoveredNode 清除状态 // 对当前 node 进行预校验 const isAllowed validateConnection(currentDragEdge.sourceNode, node); node.setState(preview, isAllowed ? allowed : not-allowed); lastHoveredNode node; } }); // 一个简单的校验函数模拟LogicFlow内部的规则检查 function validateConnection(sourceNodeModel, targetNodeModel) { // 这里需要遍历 sourceNodeModel.sourceRules 和 targetNodeModel.targetRules // 这是一个简化示例实际逻辑更复杂 for (const rule of sourceNodeModel.sourceRules) { if (!rule.validate(sourceNodeModel, targetNodeModel, null, null)) { return false; } } for (const rule of targetNodeModel.targetRules) { if (!rule.validate(sourceNodeModel, targetNodeModel, null, null)) { return false; } } return true; }实现完整的预验证悬停反馈需要较多工作因为它触及了LogicFlow内部的事件和状态管理机制。但对于追求极致用户体验的专业工具这项投入是值得的。4. 实战构建一个表单审批流程设计器让我们综合运用以上知识设计一个简化的表单审批流程设计器。这个设计器包含以下节点类型开始事件 (StartEvent)只能出不能入。用户任务 (UserTask)代表一个审批环节。入边可以来自开始事件、其他用户任务串行或网关出边只能指向网关或结束事件。并行网关 (ParallelGateway)用于发起会签。入边来自用户任务出边指向多个用户任务。排他网关 (ExclusiveGateway)用于条件分支。入边来自用户任务出边指向多个用户任务每个出口可配置条件。结束事件 (EndEvent)只能入不能出。4.1 定义节点模型与规则我们将为每种节点创建独立的模型文件。这里以UserTaskModel为例展示其规则配置的复杂性。// UserTaskModel.js class UserTaskModel extends RectNodeModel { initNodeData(data) { super.initNodeData(data); this.properties data.properties || { assignee: , formKey: }; this.style.fill #D4EDDA; this.style.stroke #C3E6CB; // 作为目标节点允许被哪些节点连接 this.targetRules.push( { message: 用户任务的前置节点只能是开始事件、用户任务或网关, validate: (source) [start-event, user-task, parallel-gateway, exclusive-gateway].includes(source.type) }, { message: 并行网关后必须连接至少两个用户任务此规则更适用于网关的sourceRules, validate: (source, target, sourceAnchor) { // 这是一个更复杂的业务规则示例如果来源是并行网关检查当前是否已有足够出边 if (source.type parallel-gateway) { const outgoingEdges source.outgoing.edges; // 简化逻辑假设并行网关必须至少连接两个任务 // 实际中可能需要更复杂的图遍历来判断 return outgoingEdges.length 4; // 假设最多支持4个分支 } return true; } } ); // 作为源节点允许连接到哪些节点 this.sourceRules.push({ message: 用户任务的后置节点只能是排他网关、并行网关或结束事件, validate: (source, target) [exclusive-gateway, parallel-gateway, end-event].includes(target.type) }); } // 可以重写getTextStyle等方法自定义外观 }4.2 实现基于角色/部门的连接限制在实际审批流中连接规则可能不仅取决于节点类型还取决于节点所承载的业务数据。例如“部门经理审批”节点后面只能连接“总监审批”或“财务审批”节点而不能连接另一个“部门经理审批”。这需要我们在节点属性properties中存储业务信息并在validate函数中访问这些信息。// 在UserTaskModel的targetRules中添加一条基于角色的规则 this.targetRules.push({ message: 同级审批角色不能相互连接, validate: (source, target) { // 假设节点属性中有 role 字段如 dept_manager, director, finance const sourceRole source.properties?.role; const targetRole this.properties?.role; // 如果目标节点是用户任务且角色相同则禁止连接 if (source.type user-task targetRole sourceRole) { return false; } // 其他情况允许 return true; } });4.3 序列化与规则的一致性维护当用户保存流程图时我们得到的是节点和边的JSON数据。这些数据本身并不包含规则。因此当在另一个会话中加载此图时必须确保注册的节点模型和规则与保存时一致。一个最佳实践是将节点类型type字符串与具体的模型类进行强绑定并通过一个中心化的注册表或依赖注入来管理避免在代码各处散落lf.register调用。此外对于极其复杂的规则例如需要查询组织架构API来判断是否允许连接validate函数可能不适合执行异步操作。在这种情况下可以考虑采用“预验证后校验”的模式前端进行基本的类型和属性校验在后端保存流程时再进行一次完整的、基于最新业务数据的规则校验并返回详细的错误信息。5. 避坑指南与性能考量在项目中使用sourceRules和targetRules时我踩过一些坑也总结了一些优化经验。1. 规则函数的性能validate函数在拖拽连线过程中会被高频调用。务必保持函数轻量避免在内部进行复杂的计算、DOM操作或同步的API调用。如果需要用到复杂数据尽量在节点初始化时计算好并缓存。2. 规则与锚点ID的唯一性自定义锚点时id必须是唯一的通常结合节点ID和锚点索引来生成如${nodeId}_anchor_${index}。如果ID重复会导致连线定位和规则校验出现不可预知的问题。3. 动态规则更新如果节点的连接规则需要根据其属性动态变化例如勾选一个复选框后允许连接更多类型的节点在修改属性后需要触发节点的更新以确保规则被重新计算和应用。有时需要调用lf.graphModel.updateNode或类似方法。4. 规则冲突与优先级当一个节点拥有多条sourceRules或targetRules时所有规则必须全部通过连接才被允许。要小心规则之间是否存在逻辑冲突。建议为规则编写清晰的message便于调试。对于复杂的业务逻辑可以考虑将多条规则合并为一条在validate函数内部进行分支判断以提高可读性和可控性。5. 与“移动规则moveRules”的区分LogicFlow还有moveRules用于控制节点移动。不要将连接规则和移动规则混淆。它们虽然机制相似但应用的场景完全不同。最后记住sourceRules和targetRules是构建自描述、自校验流程图的核心工具。它们将业务逻辑从后端校验部分地前移到了交互层面让错误在发生的那一刻就被发现和阻止。结合精心设计的可视化反馈你能打造出既专业又用户友好的图形化配置体验。在实际项目中我从最初只控制节点类型到后来基于锚点和业务属性实现字段级的连接控制这套机制始终稳定而强大。花时间设计好规则远比在用户提交错误数据后再去报错要划算得多。