LogicFlow 插件魔改实录:手把手教你重写动态分组(DynamicGroup)🛠️
写在开头
嘿嘿,大家好!👋
今是2025年12月31日上午 8 时 40 分,也是2025年最后一天了,小编做到今天,今年就不做了,感觉这行还是不适应,自己每天不知道在做些什么,天天都忙忙忙,很累很崩溃,钱也没赚到,一点热情也没了。😋
最近,在项目中重度使用了 LogicFlow,不得不说,它的扩展性是真的强。💪💪💪
但是(注意,我要说但是了😄),在使用官方提供的 动态分组(DynamicGroup)插件时,由于小编的业务场景比较特殊,遇到了一些让人头秃的问题。
本着 "开源不满足就魔改" 的极客精神,小编决定对 DynamicGroup 插件进行一次深度改造。
所以呢,今天要分享的是 DynamicGroup 插件的改造的完整过程、源码分析以及避坑指南,请诸君按需食用哈。
需求背景与痛点
最近在项目中,小编需要实现一个 "打组与拆组" 功能,不仅要能把部分节点框在一起组成一个整体分组,也需要能随时进行拆组,把子节点放出来。
基于这个需求背景,小编在 LogicFlow 官方文档上找到了两个插件:框选(SelectionSelect)与 动态分组 (DynamicGroup)。
- SelectionSelect:负责画框框选节点。
- DynamicGroup:负责把选中的节点 "打包" 成一个组。
这两个插件组合使用理论上能满足小编的业务需求,天作之合属于!🤔
不过,在实际集成过程中,小编发现它们虽然功能强大,但细节上还是有点 "水土不服",无法完全满足细腻的业务需求。于是,小编决定把这两个插件都重写了!
其中,框选插件的改动比较简单,主要是调整了一下样式和微小的交互逻辑,就不在今天展开细说了。我们重点要聊的,动态分组插件的改动过程。
下面,简述小编遇到的几个主要问题:
- "连坐"机制:官方插件默认的逻辑是,删除分组时,会强制删除分组内的所有子节点。但在小编的"拆组"业务里,用户只是想解散分组,保留里面的子节点。
-
"越狱"现象:虽然插件提供了
isRestrict: true来限制节点拖出分组,但在快速拖拽时,节点经常能"穿墙"而出,这个问题其实是和下面的问题应该是同个问题。 -
"误触"跳出:如果分组内的子节点包含输入框,点击输入框聚焦时,插件偶尔会误判为"拖拽结束(
node:drop)",导致子节点莫名其妙地被移出分组。
为了解决这些症状,小编决定给 DynamicGroup 插件开点药食食。😁
DynamicGroup插件源码分析
在动手之前,咱们先来看看 DynamicGroup 插件的构造,这个插件由以下几个核心文件组成,咱们这次改造也沿用了这个结构:
-
index.js(核心逻辑):插件的入口,负责注册插件和监听全局事件(如node:add,node:drop)。它维护了一个映射表,决定了节点什么时候进分组,什么时候出分组。
🔉插播一下,插件源码维护得挺好,有非常清晰的代码注释,再结合AI的协助,让人非常容易理解。👏
-
model.js(数据模型):定义分组的数据属性。比如isRestrict(是否限制拖拽)、isCollapsed(是否折叠)等状态都是在这里管理的。 -
node.js(视图渲染):负责画出分组的样子(SVG)。这里也是离用户最近的地方,处理具体的鼠标交互事件(如mousemove)。 -
utils.js(工具函数):提供一些计算几何关系的辅助方法,比如判断"这个点是不是在那个框里"。
搞清楚了这些,咱们就可以对症下药了!
改造一:解决"连坐"机制
痛点描述
官方默认的逻辑是:分组被删除 = 分组内的所有子节点一起被删除。
如下:
![]()
但在很多业务场景下,用户点击删除分组,可能只是想"解散"这个组,而不是把里面的业务子节点也删了。
源码定位
打开 index.js,找到 removeNodeFromGroup 方法:
// 官方源码逻辑
removeNodeFromGroup = ({ data: node, model }) => {
if (model.isGroup && node.children) {
node.children.forEach((childId) => {
this.nodeGroupMap.delete(childId);
this.lf.deleteNode(childId); // 👈 在这里!直接把子节点干掉了
});
}
// ...
};
改造方案
咱们需要引入一个配置项 retainChildren(保留子节点)。在删除前,检查一下这个属性,如果为 true,就只解除关系,不删节点。
为什么要如此做❓
因为小编还有另外的业务需求是右键删除节点功能,这个操作就需要把子节点也一起删除了,所以增加配置控制的形式更加灵活一些。
修改 index.js 文件:
// 改造后代码逻辑
removeNodeFromGroup = ({ data: node, model }) => {
// 获取配置属性
const retainChildren = model.properties && model.properties.retainChildren;
if (model.isGroup && node.children) {
node.children.forEach((childId) => {
this.nodeGroupMap.delete(childId); // 解除映射关系
// 关键判断:只有不保留时,才删除子节点
if (!retainChildren) {
this.lf.deleteNode(childId);
}
});
}
// ... 后续逻辑不变
};
这样,我们在创建分组时,只要加上 properties: { retainChildren: true },就能实现"拆组"效果了。
![]()
改造二:拒绝"误触"跳出
痛点描述
如果你的节点里包含输入框(HTML 节点),当你点击输入框聚焦时,LogicFlow 可能会触发 node:drop 事件(因为它认为你完成了一次交互)。
官方插件在监听 node:drop 时,会重新计算节点应该属于哪个分组。结果就是:点了一下输入框,插件误判你把节点移走了,直接把它踢出了分组。
![]()
源码定位
打开 index.js 文件,找到 addNodeToGroup 方法:
// 官方源码逻辑
addNodeToGroup = (node) => {
// ... 计算节点当前的位置 bounds
const preGroupId = this.nodeGroupMap.get(node.id); // 原来的组
const targetGroup = this.getGroupByBounds(bounds, node); // 现在位置对应的组
if (preGroupId) {
// 👈 问题在这里:只要有原分组,它就默认先移除,再看要不要加入新组
// 如果计算有些许误差,或者逻辑不够严谨,节点就“丢”了
const group = this.lf.getNodeModelById(preGroupId);
group.removeChild(node.id);
this.nodeGroupMap.delete(node.id);
}
// ...
};
这个方法会在 node:drop 事件被触发时被调用。
改造方案
逻辑很简单:如果节点原来的组和现在的组是同一个,那就啥也别动! 稳住别浪!🌊
修改 index.js 文件:
// 改造后代码逻辑
addNodeToGroup = (node) => {
// ... 前面获取 bounds 逻辑不变
const preGroupId = this.nodeGroupMap.get(node.id);
const targetGroup = this.getGroupByBounds(bounds, node);
const targetGroupId = targetGroup ? targetGroup.id : undefined;
// 新增核心判断,直接 return 掉!
if (preGroupId === targetGroupId) {
return;
}
// ... 下面才是真正的移动逻辑
if (preGroupId) {
// ... 移除旧组逻辑
}
if (targetGroup) {
// ... 加入新组逻辑
}
};
这样子就搞定了:
![]()
改造三:"越狱"现象之绝对防御
痛点描述
DynamicGroup 插件支持设置 isRestrict: true 来限制子节点不能拖出分组。
![]()
这个问题挺奇怪的,小编在群里反馈给过官方维护人员,但是他们好像没有定位到这个问题,在小编本地也确实比较难复现,只会偶尔出现。但是呢,小编的测试同学却能一次一次的复现给小编看,这种问题最难受了。😭
面对铁证,小编也很无奈,只能请AI来协助了,我让AI大致分析了整个源码情况。
限制子节点的拖动范围原理大概是在 graphModel.addNodeMoveRules 里加规则。
但是!这个规则校验是基于"下一次位置是否合法"来拦截的。如果你鼠标甩得特别快(比如高 DPI 鼠标),deltaX/deltaY 很大,直接跳过了边界检测,节点就会"穿墙"而出。
源码定位
这次咱们要去视图层 node.js 文件。因为规则拦截(Model 层)已经防不住了,我们必须在渲染层(View 层)做最后的兜底。
我们需要监听 node:mousemove 事件,这是节点移动最频繁触发的地方。
改造方案
在 node.js 文件中,我们重写 onNodeMouseMove 方法,并在组件挂载时(componentDidMount)监听它。
逻辑核心:
- 实时计算节点的新位置。
- 判断是否超出了分组的边界。
- 如果超出,强制将坐标修正回边界内(暴力修正)。
- ⏰同步更新连线(这一点很容易漏,如果不更连线,节点回去了,线还在外面)。
修改 node.js 文件:
// 改造后代码逻辑
// 1. 在 component 绑定事件
componentDidMount() {
super.componentDidMount();
// 监听更底层的 mousemove
this.props.graphModel.eventCenter.on("node:mousemove", this.onNodeMouseMove);
}
// 2. 核心处理逻辑
onNodeMouseMove({ data }) {
const { model: curGroup, graphModel } = this.props;
const model = graphModel.getNodeModelById(data.id); // 当前拖动的节点
// 只有开启了限制,且是自家孩子,才管
if (curGroup.children.has(model.id) && curGroup.isRestrict) {
const groupBounds = curGroup.getBounds();
const nodeBounds = model.getBounds();
const padding = 10; // 内边距,别贴得太死
let newX = model.x;
let newY = model.y;
// X轴 暴力修正
if (nodeBounds.minX < groupBounds.minX + padding) {
newX = groupBounds.minX + padding + model.width / 2;
} else if (nodeBounds.maxX > groupBounds.maxX - padding) {
newX = groupBounds.maxX - padding - model.width / 2;
}
// Y轴 暴力修正
if (nodeBounds.minY < groupBounds.minY + padding) {
newY = groupBounds.minY + padding + model.height / 2;
} else if (nodeBounds.maxY > groupBounds.maxY - padding) {
newY = groupBounds.maxY - padding - model.height / 2;
}
// 如果位置被我们强行修正了
if (newX !== model.x || newY !== model.y) {
// 移动节点
model.moveTo(newX, newY);
// 关键:手动更新连线!
// 否则会出现节点被墙挡住了,但连线跟着鼠标飞出去了的诡异画面,非常神奇💥
this.updateRelatedEdges(model, graphModel);
}
}
}
// 辅助方法:更新连线
updateRelatedEdges(model, graphModel) {
const edges = graphModel.getNodeEdges(model.id);
edges.forEach((edge) => {
// 重新计算并设置连线的起点/终点
if (edge.sourceNodeId === model.id) {
// ... updateStartPoint 逻辑
}
if (edge.targetNodeId === model.id) {
// ... updateEndPoint 逻辑
}
});
}
这里的改造大部分是AI在帮我写的,我只是最终确定一下代码逻辑的合理性,与没有太离谱和边界把控,还需要在页面进行测试验证,最终,确定基本没有什么问题,才交由小编的测试同学去验证,最终也顺利通过验证。😀
这里还是得表扬AI一番啊,这个问题前后大概花了十几二十分钟就搞定了,要是没有AI,靠人工来解决这个问题,时间上应该不敢想象吧。🤔
总结
通过对 DynamicGroup 插件的这番改造,咱们不仅解决了一系列交互 Bug,更重要的是深入理解了 LogicFlow 插件的运行机制。
- Model 层 负责数据准确性(如删除逻辑)。
- Logic 层(index.js)负责业务流转(如分组进出判断)。
- View 层 负责极致的交互体验(如拖拽边界修正)。
希望这篇踩坑实录能给正在使用 LogicFlow 的小伙伴们一些灵感,嘿嘿。😉
至此,本篇文章就写完啦,撒花撒花。
![]()
希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。