前言
liteflow是一款轻量、快速、稳定可编排的组件式规则引擎,可编排是liteflow非常重要的特性,甚至是为编排而生。目前liteflow官方支持的流程编排还是主要依靠编写el表达式来实现,因此分享一下我实现的liteflow组件可视化编排的思路。
liteflow组件特点
先来看一个简单el表达式的例子:
css
THEN(
a,
WHEN(b, c, d),
e
);
对应的流程图如下:
THEN
和WHEN
是liteflow的组件关键字,被THEN
包裹的节点会串行执行,被WHEN
包裹的节点会并行执行,这个el表达式解析后,先执行a,再并行执行b,c,d,默认并行都执行完毕后,执行e。
在实现通过ui拖拽进行编排时,如果生成像上面的这样的流程图,我们发现其实这个流程图隐藏了一些信息:
- 是a执行完后是有一个开启并行执行的动作
- b,c,d执行完毕后有个聚合join的动作
这些信息在简单的流程场景下不是必要的,因为可以通过流程图的节点连接信息进行推导,但是在有大量嵌套的并行组件之后,想要把这信息正确推导出来就会比较复杂,例如下面这个例子: 这张图来自gitee上一个项目的issues
完善编排流程图
接下来我们在流程图中把el表达式中的信息都展示出来:
引入了两个图形用于表示WHEN
并行组件的入口和出口,这样无论有多少嵌套的并行组件都能很容易的解析出来并转换成el表达式。
可能有些同学会觉得对于使用者来说无需关心WHEN
并行组件的入口和出口,觉得这在图形上展示是多余的,其实不然,因为显式的标注出WHEN
并行组件的入口和出口在多层嵌套的场景下,会让各个组件之间的边界更加清晰,其次当我们使用并行组件的时候,我们需要对并行组件根据业务要求进行配置,这在liteflow里也是支持的:
- 忽略错误
- 任一节点先执行完则忽略其他
- 指定任意节点先执行完则忽略其他
UI交互
前面我们说到liteflow的组件是必须闭合的,因此我们可以在ui操作上进行约束,当添加并行组件时,直接在流程图中把并行组件的出口、入口以及默认分支全部创建好:
对UI操作约束能够让使用的人不会因为不熟悉liteflow的组件特点而导致生成不适配liteflow的编排流程图,而且操作更加高效。
代码实现
这里前端以logicflow为例,前端通过UI编排好组件之后调用getGraphData()
获取logicflow的流程绘图数据如下:
json
{
"nodes": [{
"id": "init_6d5b8qfcwl00000",
"type": "EventNode",
"properties": {
"name": "页面初始化"
}
}, {
"id": "0",
"type": "ParallelGateway",
"properties": {
"name": "并行组件0",
"joinNode": "3"
}
}, {
"id": "1",
"type": "CommonNode",
"properties": {
"name": "1"
}
}, {
"id": "3",
"type": "JoinGateway",
"properties": {
"name": "join3"
}
}, {
"id": "2",
"type": "CommonNode",
"properties": {
"name": "2"
}
}],
"edges": [{
"sourceNodeId": "1",
"targetNodeId": "3"
}, {
"sourceNodeId": "2",
"targetNodeId": "3"
}, {
"sourceNodeId": "0",
"targetNodeId": "1"
}, {
"sourceNodeId": "0",
"targetNodeId": "2"
}, {
"sourceNodeId": "init_6d5b8qfcwl00000",
"targetNodeId": "0"
}]
}
这里我把数据精简了,只留下必要的数据,之后就是要将前端绘图数据转换成liteflow能够使用的数据。
通过debug liteflow源码,知道el表达式的解析之后生成的内存数据结构如下:
了解了liteflow内存中编排数据的结构,我们可以直接解析前端的绘图数据来生成liteflow需要的流程数据,而不需要先生成liteflow的el表达式,再让liteflow解析一遍。
liteflow使用LiteFlowChainELBuilder
类来解析el表达式、组装规则链,我们可以模仿创建一个LiteFlowChainLogicFlowBuilder
来解析前端生成的编排绘图数据,再组装生成liteflow能够使用的规则链。
对并行组件的处理:
java
// 前端绘图数据当前节点为when组件时
private void whenCondition(Condition parentCondition, LogicFlowGraphData.Node node) {
WhenCondition whenCondition = new WhenCondition();
whenCondition.setParallelStrategy(ParallelStrategyEnum.ALL);
parentCondition.addExecutable(whenCondition);
// 1.获取并行组件的所有分支第一个节点,每个分支使用ThenCondition组件包裹
List<LogicFlowGraphData.Node> outgoingNodes = this.getNodeOutgoingNodes(node.id);
outgoingNodes.forEach(outgoingNode -> {
ThenCondition thenCondition = this.createThenCondition(whenCondition);
this.commonNode(thenCondition, outgoingNode);
});
// 2.获取并行组件的join节点,再处理join节点的后续节点数据
LogicFlowGraphData.Node joinNode = this.nodeMap.get(node.properties.get("joinNode"));
List<LogicFlowGraphData.Node> joinNodeOutgoingNodes = this.getNodeOutgoingNodes(joinNode.id);
joinNodeOutgoingNodes.forEach(outgoingNode -> {
if (outgoingNode.type.equals("ParallelGateway")) {
this.whenCondition(parentCondition, outgoingNode);
} else if (outgoingNode.type.equals("JoinGateway")) {
// TODO 处理并行组件配置数据
} else {
ThenCondition thenCondition = this.createThenCondition(parentCondition);
this.commonNode(thenCondition, outgoingNode);
}
});
}
对普通任务节点的处理:
java
// 前端绘图数据当前节点为普通任务节点
private void commonNode(Condition parentCondition, LogicFlowGraphData.Node node) {
// 创建普通任务节点,配置节点的名称、id等数据,也可能是脚本节点,处理方式基本一致
NodeComponent nodeComponent = new NodeComponent() {
@Override
public void process() throws Exception {
System.out.println(node.properties.get("name"));
}
};
nodeComponent.setSelf(nodeComponent);
Node liteFlowNode = new Node(nodeComponent);
parentCondition.addExecutable(liteFlowNode);
// 处理后续节点
List<LogicFlowGraphData.Node> outgoingNodes = this.getNodeOutgoingNodes(node.id);
outgoingNodes.forEach(outgoingNode -> {
if (outgoingNode.type.equals("ParallelGateway")) {
this.whenCondition(parentCondition, outgoingNode);
} else if (outgoingNode.type.equals("JoinGateway")) {
// TODO 处理并行组件配置数据
} else {
this.commonNode(parentCondition, outgoingNode);
}
});
}
主要介绍这两个节点的解析,其他选择组件、条件组件与并行组件处理方式基本一致。