news 2026/10/20 18:11:48

LiteFlow与LogicFlow整合:可视化规则配置与EL表达式生成实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LiteFlow与LogicFlow整合:可视化规则配置与EL表达式生成实战

简介:这是一份面向Web应用开发者的前后端协同配置方案,聚焦规则引擎Liteflow与前端流程图库LogicFlow的整合实践。资源提供一套可视化流程配置页面,用户可在界面上拖拽节点、连线并定义规则,配置结果以JSON格式输出,交由后端解析并生成Liteflow可执行的EL表达式,从而把复杂业务逻辑转化为可维护的自动化流程,适合具备Vue与TypeScript基础、希望理解规则引擎落地方式的中级开发者。压缩包共22个文件,约180KB,以9个ts脚本、2个vue组件、4个json配置为主,另含html入口、png与jpg示意图及ico图标等,结构紧凑,便于快速阅读源码与二次改造。目前已有1497人学习下载。通过该资源可掌握LogicFlow图形编辑与数据序列化、JSON到EL的转换思路以及前后端职责划分,对搭建可视化规则配置平台具有直接参考价值。

1. 规则引擎 LiteFlow 配 LogicFlow:前端画完图,后端直接跑 EL

如果你正在做审批流、风控策略、工单路由这类“业务规则天天变”的系统,大概率听过 LiteFlow 这个名字。它把规则写成 EL 表达式,后端解析执行,改规则不用重启。但问题来了:EL 表达式让运营同学手写,等于把线上事故的遥控器交出去。于是就有了这套组合拳——前端用 LogicFlow 画流程图,页面配置完导出 JSON,后端把 JSON 翻译成 LiteFlow 能吃的 EL 表达式。听起来很顺,但真动手你会发现,LogicFlow 的节点数据结构和 LiteFlow 的 EL 语法之间隔着一整条鸿沟。这篇笔记就讲怎么把这条鸿沟填平,从节点定义、JSON 结构设计,到 EL 生成、后端解析,再到几个我踩过的血泪坑。适合有 LiteFlow 使用经验、想给业务方做一个可视化规则配置页的前后端工程师。

2. 为什么选 LogicFlow 而不是手写 EL:选型与数据结构设计

2.1 手写 EL 的三个致命问题

LiteFlow 的 EL 表达式长这样:THEN(a, b, WHEN(c, d), e)。简单场景下确实优雅,但一旦规则超过三层嵌套,运营同学基本就放弃了。我见过最离谱的一个表达式,嵌套了七层,括号匹配全靠编辑器高亮,改一个条件要花半小时确认没改错。

具体来说,手写 EL 有三个绕不过去的问题。第一,语法门槛高。THEN、WHEN、SWITCH、IF这些关键字对非技术人员就是天书,更别说THEN(a, WHEN(b, c), d)这种嵌套语义。第二,无法校验。运营写错了括号或者漏了逗号,只有运行时才报错,而这时候可能已经影响到线上业务了。第三,不可视化。规则之间的依赖关系藏在字符串里,新人接手要花大量时间梳理。

LogicFlow 解决的就是这三个问题。它把每个组件变成一个可拖拽的节点,连线代表执行顺序,嵌套关系用分组或者子流程表达。运营只需要关心“先做什么、再做什么、什么条件下走哪条路”,不需要知道底层是THEN还是WHEN。

2.2 LogicFlow 的节点模型怎么映射到 LiteFlow 组件

LogicFlow 的核心数据是nodes和edges两个数组。每个节点有id、type、x、y、properties这些字段。我们要做的映射是:节点类型对应 LiteFlow 的组件类型,节点属性对应组件参数,连线关系对应执行顺序。

我一般会定义四类节点。第一类是COMMON节点,对应 LiteFlow 的普通组件,执行具体的业务逻辑。第二类是IF节点,对应条件判断,它的properties里存条件表达式。第三类是SWITCH节点,对应多路选择。第四类是PARALLEL节点,对应WHEN并行执行。

这里有个关键设计决策:LogicFlow 的节点是扁平的,但 LiteFlow 的 EL 是嵌套的。怎么把扁平结构转成嵌套表达式?我的做法是,在节点属性里加一个parentId字段,用来标识它属于哪个父节点。这样在生成 EL 的时候,可以递归地把子节点组装成嵌套结构。

// 节点数据结构示例 const nodeData = { id: 'node_1', type: 'COMMON', // 节点类型:COMMON / IF / SWITCH / PARALLEL x: 300, y: 200, properties: { componentId: 'checkInventory', // 对应 LiteFlow 组件 bean 名称 parentId: null, // 父节点 id,用于构建嵌套关系 condition: '', // IF 节点专用:条件表达式 branches: [] // SWITCH 节点专用:分支列表 } };

这段代码定义了节点的基本结构。componentId是后端 LiteFlow 组件的 Spring Bean 名称,必须一一对应。parentId是构建嵌套 EL 的关键,没有它就只能生成平铺的THEN链,无法表达条件分支。condition和branches只在特定类型节点上使用。

2.3 用 LogicFlow 注册自定义节点

LogicFlow 默认只有矩形、圆形这些基础节点,我们需要注册业务节点。注册的时候要定义节点的视图和模型。视图决定长什么样,模型决定数据怎么存。

import { RectNode, RectNodeModel } from '@logicflow/core'; // 自定义 COMMON 节点视图 class CommonNodeView extends RectNode { // 可以在这里自定义节点渲染,比如加图标 } // 自定义 COMMON 节点模型 class CommonNodeModel extends RectNodeModel { setAttributes() { this.width = 160; this.height = 60; this.radius = 8; // 从 properties 里读取组件名称显示在节点上 this.text = this.properties.componentName || '未命名组件'; } } // 注册节点 lf.register({ type: 'COMMON', view: CommonNodeView, model: CommonNodeModel });

注册节点的核心是type字段,它必须和后面 EL 生成逻辑里的类型判断一致。setAttributes里可以动态设置节点样式和文本,我一般会把组件的中文名称显示在节点上,方便运营识别。注意this.properties是响应式的,修改后节点会自动更新。

3. 从画布到 JSON:页面配置的完整数据流

3.1 画布初始化与节点拖拽配置

页面初始化的时候,需要把后端已有的规则 JSON 反序列化成 LogicFlow 能识别的图数据。如果是新建规则,就给一个空的nodes和edges数组。

import LogicFlow from '@logicflow/core'; import '@logicflow/core/dist/style/index.css'; const lf = new LogicFlow({ container: document.querySelector('#canvas'), grid: true, keyboard: { enabled: true }, style: { nodeText: { overflowMode: 'ellipsis' } } }); // 如果有已有数据,渲染到画布 if (existingGraphData) { lf.render(existingGraphData); } // 监听节点添加事件,给新节点设置默认属性 lf.on('node:add', ({ data }) => { const { properties } = data; if (!properties.componentId) { // 弹出组件选择框,让用户选择要执行的组件 showComponentSelector(data.id); } });

lf.render()接收的就是标准的 LogicFlow 图数据,包含nodes和edges。node:add事件在用户拖拽新节点到画布时触发,这时候需要引导用户选择具体的业务组件。我一般会弹一个对话框,列出后端注册的所有 LiteFlow 组件,用户选完后把componentId和componentName写回节点属性。

3.2 节点属性面板与条件配置

选中节点后,右侧属性面板要能编辑节点的详细配置。不同类型的节点,面板内容不一样。COMMON节点只需要选组件,IF节点需要配条件表达式,SWITCH节点需要配多个分支。

// 监听节点选中事件 lf.on('node:click', ({ data }) => { const { type, properties } = data; // 根据节点类型渲染不同的属性面板 switch (type) { case 'COMMON': renderCommonPanel(properties); break; case 'IF': renderIfPanel(properties); break; case 'SWITCH': renderSwitchPanel(properties); break; case 'PARALLEL': renderParallelPanel(properties); break; } }); // IF 节点条件配置示例 function renderIfPanel(properties) { // 条件表达式输入框 const conditionInput = document.querySelector('#condition-input'); conditionInput.value = properties.condition || ''; conditionInput.addEventListener('change', (e) => { // 更新节点属性 lf.setProperties(nodeId, { condition: e.target.value }); }); }

属性面板的核心是lf.setProperties()方法,它能把面板上的修改实时同步到画布数据。IF节点的条件表达式我一般用简单的比较运算,比如orderAmount > 1000,后端解析的时候再转换成 LiteFlow 的条件组件。注意条件表达式不要做得太复杂,否则又回到了手写 EL 的老路。

3.3 导出 JSON:数据结构与校验

配置完成后,点击“保存”按钮,需要把画布数据导出成后端能解析的 JSON。导出的数据要包含节点、连线、以及每个节点的完整属性。

function exportGraphData() { const graphData = lf.getGraphData(); // 校验:检查是否有孤立节点 const nodeIds = new Set(graphData.nodes.map(n => n.id)); const connectedIds = new Set(); graphData.edges.forEach(e => { connectedIds.add(e.sourceNodeId); connectedIds.add(e.targetNodeId); }); const isolatedNodes = graphData.nodes.filter( n => !connectedIds.has(n.id) && n.type !== 'START' ); if (isolatedNodes.length > 0) { throw new Error(`存在未连接的节点:${isolatedNodes.map(n => n.id).join(', ')}`); } // 校验:IF 节点必须有条件表达式 const invalidIfNodes = graphData.nodes.filter( n => n.type === 'IF' && !n.properties.condition ); if (invalidIfNodes.length > 0) { throw new Error('IF 节点缺少条件表达式'); } return { nodes: graphData.nodes.map(n => ({ id: n.id, type: n.type, properties: n.properties })), edges: graphData.edges.map(e => ({ source: e.sourceNodeId, target: e.targetNodeId, sourceAnchor: e.sourceAnchorId, targetAnchor: e.targetAnchorId })) }; }

导出前必须做校验,否则后端拿到脏数据直接生成错误的 EL,排查起来非常痛苦。我一般会校验三点:有没有孤立节点、IF 节点有没有条件、SWITCH 节点有没有分支。lf.getGraphData()返回的数据包含了很多渲染相关的字段,导出时只需要保留id、type、properties和连线关系,这样 JSON 体积小,后端解析也快。

4. 后端把 JSON 翻译成 EL:递归组装与组件映射

4.1 解析 JSON 构建节点关系图

后端拿到前端传来的 JSON 后,第一步是构建节点关系图。用邻接表存储节点之间的指向关系,方便后续递归遍历。

public class GraphParser { public static class Node { public String id; public String type; public Map<String, Object> properties; public List<String> nextNodes = new ArrayList<>(); } public Map<String, Node> parseGraph(GraphJson json) { Map<String, Node> nodeMap = new HashMap<>(); // 先把所有节点放入 map for (NodeJson nodeJson : json.getNodes()) { Node node = new Node(); node.id = nodeJson.getId(); node.type = nodeJson.getType(); node.properties = nodeJson.getProperties(); nodeMap.put(node.id, node); } // 再根据边构建指向关系 for (EdgeJson edge : json.getEdges()) { Node source = nodeMap.get(edge.getSource()); if (source != null) { source.nextNodes.add(edge.getTarget()); } } return nodeMap; } }

这段代码把扁平的节点和边转成了有向图。nextNodes列表存储了每个节点的后继节点。注意这里没有处理环的情况,如果前端画了循环,递归生成 EL 的时候会栈溢出。所以我在解析前会做一次环检测,有环直接拒绝。

4.2 递归生成 EL 表达式的核心算法

生成 EL 的核心是递归。从起始节点开始,根据节点类型决定用THEN、WHEN还是IF包裹子表达式。

public class ElGenerator { public String generate(Map<String, Node> nodeMap, String startNodeId) { Node start = nodeMap.get(startNodeId); if (start == null) { throw new IllegalArgumentException("起始节点不存在"); } return buildExpression(nodeMap, start); } private String buildExpression(Map<String, Node> nodeMap, Node node) { switch (node.type) { case "COMMON": return buildCommon(node, nodeMap); case "IF": return buildIf(node, nodeMap); case "SWITCH": return buildSwitch(node, nodeMap); case "PARALLEL": return buildParallel(node, nodeMap); default: throw new IllegalArgumentException("未知节点类型:" + node.type); } } private String buildCommon(Node node, Map<String, Node> nodeMap) { String componentId = (String) node.properties.get("componentId"); if (node.nextNodes.isEmpty()) { return componentId; } // 有后继节点,用 THEN 串联 List<String> parts = new ArrayList<>(); parts.add(componentId); for (String nextId : node.nextNodes) { parts.add(buildExpression(nodeMap, nodeMap.get(nextId))); } return "THEN(" + String.join(", ", parts) + ")"; } private String buildIf(Node node, Map<String, Node> nodeMap) { String condition = (String) node.properties.get("condition"); // IF 节点需要两个分支:true 和 false // 这里简化处理,假设 nextNodes 第一个是 true 分支,第二个是 false 分支 String trueBranch = buildExpression(nodeMap, nodeMap.get(node.nextNodes.get(0))); String falseBranch = node.nextNodes.size() > 1 ? buildExpression(nodeMap, nodeMap.get(node.nextNodes.get(1))) : "DEFAULT"; return "IF(" + condition + ", " + trueBranch + ", " + falseBranch + ")"; } private String buildParallel(Node node, Map<String, Node> nodeMap) { List<String> branches = new ArrayList<>(); for (String nextId : node.nextNodes) { branches.add(buildExpression(nodeMap, nodeMap.get(nextId))); } return "WHEN(" + String.join(", ", branches) + ")"; } }

这段代码是核心。buildCommon处理普通节点,如果有后继节点就用THEN串联。buildIf处理条件节点,生成IF(条件, 真分支, 假分支)结构。buildParallel处理并行节点,生成WHEN(分支1, 分支2, ...)。注意buildIf里假设了nextNodes的顺序,实际项目中最好在节点属性里明确标记哪个是 true 分支、哪个是 false 分支,避免依赖连线顺序。

4.3 组件注册与 EL 执行

生成的 EL 字符串最终要交给 LiteFlow 执行。LiteFlow 需要知道每个componentId对应哪个 Spring Bean。

@Component("checkInventory") public class CheckInventoryCmp extends NodeComponent { @Override public void process() { // 业务逻辑:检查库存 OrderContext context = this.getContextBean(OrderContext.class); boolean hasStock = inventoryService.check(context.getOrderId()); context.setHasStock(hasStock); } }

每个组件继承NodeComponent,实现process方法。@Component注解里的名称必须和前端配置的componentId一致。LiteFlow 启动时会扫描所有NodeComponent子类,注册到组件工厂里。执行的时候,解析 EL 表达式,按顺序调用对应的组件。

// 执行规则 LiteflowResponse response = flowExecutor.execute2Resp("chain1", orderContext); if (!response.isSuccess()) { log.error("规则执行失败:{}", response.getMessage()); }

execute2Resp是 LiteFlow 的响应式执行方法,返回LiteflowResponse包含执行结果和上下文。注意chain1是规则链的名称,需要和 LiteFlow 配置里的链名对应。

5. 避坑指南:五个让我加班到凌晨的坑

5.1 坑一:节点 ID 重复导致 EL 生成错乱

现象:前端画布上复制粘贴节点后,生成的 EL 里同一个组件出现了两次,但逻辑上应该只执行一次。

原因:LogicFlow 复制节点时,新节点的id默认是随机生成的,但如果用户手动改了id或者从外部导入数据时没检查,就会出现重复。后端用Map<String, Node>存储节点,重复 ID 会覆盖,导致部分节点丢失。

解决:在导出 JSON 前做一次 ID 唯一性校验,发现重复直接拒绝并提示用户。后端解析时也加一层校验,双重保险。

function validateNodeIds(nodes) { const idSet = new Set(); const duplicates = []; nodes.forEach(n => { if (idSet.has(n.id)) { duplicates.push(n.id); } idSet.add(n.id); }); if (duplicates.length > 0) { throw new Error(`节点 ID 重复:${duplicates.join(', ')}`); } }

5.2 坑二:IF 节点分支顺序依赖连线锚点

现象:用户把 true 分支的线连到了节点底部,false 分支连到了右侧,生成的 EL 里两个分支反了。

原因:buildIf方法里直接按nextNodes的顺序取 true 和 false 分支,但nextNodes的顺序取决于边的遍历顺序,而边的顺序又取决于用户先连了哪条线。用户先连 false 分支,false 就变成了第一个。

解决:在 IF 节点的属性里显式标记分支类型。连线的时候,根据锚点位置判断是 true 还是 false,写入边的properties里。

lf.on('edge:add', ({ data }) => { const sourceNode = lf.getNodeModelById(data.sourceNodeId); if (sourceNode.type === 'IF') { // 根据锚点位置判断分支类型 const anchorId = data.sourceAnchorId; const branchType = anchorId.includes('right') ? 'true' : 'false'; lf.setEdgeProperties(data.id, { branchType }); } });

5.3 坑三:SWITCH 节点缺少默认分支导致执行异常

现象:SWITCH 节点的所有条件都不满足时,LiteFlow 抛异常,提示没有匹配的分支。

原因:LiteFlow 的SWITCH表达式要求必须有一个DEFAULT分支,否则条件不匹配时无法决定走哪条路。前端配置的时候,用户可能只配了两个条件分支,没配默认分支。

解决:前端在导出 SWITCH 节点时,自动检查有没有默认分支,没有就自动补一个DEFAULT分支,指向一个空组件或者直接结束。

private String buildSwitch(Node node, Map<String, Node> nodeMap) { List<String> branches = new ArrayList<>(); boolean hasDefault = false; for (String nextId : node.nextNodes) { Node next = nodeMap.get(nextId); String branchExpr = buildExpression(nodeMap, next); branches.add(branchExpr); if ("DEFAULT".equals(next.properties.get("branchType"))) { hasDefault = true; } } if (!hasDefault) { branches.add("DEFAULT"); } return "SWITCH(" + String.join(", ", branches) + ")"; }

5.4 坑四:并行节点里的组件共享上下文导致数据覆盖

现象:WHEN并行执行的两个组件都往上下文里写同一个字段,后执行的覆盖了先执行的。

原因:LiteFlow 的WHEN是并行执行,多个组件同时操作同一个上下文对象,如果没有做隔离,就会出现竞态条件。

解决:并行分支里的组件不要写同一个上下文字段。如果必须写,用不同的字段名,或者在组件内部加锁。更好的做法是,并行分支只读上下文,把结果写到各自的局部变量里,最后由一个汇总组件统一写入。

5.5 坑五:前端画布数据量大了之后渲染卡顿

现象:规则节点超过 200 个之后,拖拽节点明显掉帧,连线延迟。

原因:LogicFlow 默认会对每个节点做完整的 SVG 渲染,节点多了之后 DOM 数量爆炸。

解决:开启 LogicFlow 的虚拟渲染或者分页加载。如果节点确实很多,建议拆成多个子流程,用子流程节点引用,而不是把所有节点画在一张图上。

const lf = new LogicFlow({ container: document.querySelector('#canvas'), // 开启局部渲染,只渲染视口内的节点 partial: true, // 关闭动画,减少渲染压力 animation: false });

6. 进阶技巧:让配置页面更好用的三个细节

第一个细节是节点分组。当规则复杂到一定程度,画布上会有大量节点,找起来很费劲。我一般会加一个分组功能,把相关的节点框在一起,分组信息存在节点的properties.groupId里。导出 JSON 的时候,分组信息也带上,后端生成 EL 的时候可以按分组做注释,方便排查。

第二个细节是实时预览 EL。用户配置完规则后,点击“预览”按钮,前端直接调用后端的 EL 生成接口,把生成的 EL 表达式显示在一个只读文本框里。这样用户能直观看到自己画的图对应什么表达式,出问题的时候也方便定位。实现上就是前端把 JSON 发给后端,后端调ElGenerator返回字符串。

第三个细节是版本对比。规则配置页面一定要有版本管理,每次保存生成一个新版本,支持回滚和对比。我一般用version字段标记,前端展示两个版本的差异,用颜色标出新增和删除的节点。这个功能在出故障的时候就是后悔药,能快速定位是哪次修改引入的问题。

最后一个习惯:每次改完 EL 生成逻辑,我都会拿一个最复杂的规则跑一遍回归测试。测试用例就是之前踩过坑的那些场景——重复 ID、分支反序、缺少默认分支。这些用例跑通了,才敢发布。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 18:10:19

51单片机驱动16x16点阵屏:动态扫描与595级联实战详解

做51单片机的小项目&#xff0c;点阵屏应用是个绕不开的坎。一块16x16点阵屏&#xff0c;就是由256颗LED组成的“灯阵”&#xff0c;看着不起眼&#xff0c;却能把单片机最核心的三样基本功全串起来&#xff1a;引脚不够时怎么扩展、时序怎么精确控制、图像怎么动态刷新。很多朋…

作者头像 李华
网站建设 2026/10/20 18:08:42

GEE位运算实战:遥感QA波段云掩膜高效提取指南

1. 项目概述&#xff1a;位运算不是“位”于天边的玄学&#xff0c;而是GEE里最常被低估的像素级手术刀在Google Earth Engine&#xff08;GEE&#xff09;里写过image.gt(0).selfMask()的人不少&#xff0c;但真正把image.select(B5).and(image.select(B4))当日常操作来用的&a…

作者头像 李华
网站建设 2026/10/20 18:08:23

开源Agent五层地图:从模型底座到应用交互的完整解剖

2026年刚开工那阵儿&#xff0c;我给自己定了一个还算疯狂的目标&#xff1a;把过去两年冒出来的Agent开源项目尽可能翻一遍。代码托管平台上每天都会多出几十个新仓库&#xff0c;标题带着agent、bot、copilot、assistant的字样&#xff0c;单看简介个个都能颠覆世界。但等我真…

作者头像 李华
网站建设 2026/10/20 18:08:02

危化园区高风险作业三维安全半径动态校验系统实战解析

在危化品园区干过安全管理的人都有体会&#xff0c;高风险作业的管控&#xff0c;最难的不是制度&#xff0c;而是“摸不着”的风险边界。动火、受限空间、高处作业这些场景&#xff0c;过去我们靠什么判断风险范围&#xff1f;监护人站在警戒线外&#xff0c;用肉眼估个大概&a…

作者头像 李华
网站建设 2026/10/20 18:07:41

专科生论文降AIGC率:千笔与锐智AI实测对比

又到了毕业季和期末季&#xff0c;后台私信里问得最多的就是两类降AIGC软件怎么选。专科生的论文要求虽然不像本科和研究生那么卷&#xff0c;但学校对AIGC检测率的红线一点没松&#xff0c;不少同学拿着千笔降AIGC助手和锐智AI的截图来问我到底哪个更稳。这两款我都实际掏钱测…

作者头像 李华
网站建设 2026/10/20 18:07:18

Locust压测实战:命令行参数详解与分布式压测高阶指南

上周给一个内部服务做上线前的容量评估&#xff0c;我直接在命令行敲了几条 locust 命令&#xff0c;几分钟后拿到了 P95、RPS 和失败率数据。同事问我为什么不用现成的压测平台&#xff0c;我说&#xff1a;因为整个压测链路就藏在 Python 脚本里&#xff0c;环境有 Python 就…

作者头像 李华