背景
在上一篇文章中给大家分享了个人开发的Mars零代码平台,其中它的事件流是基于流程节点的形式开发的,非常有意思,相比于传统的低代码事件交互有很多优点。那本篇文章想介绍一下,Mars中的事件流交互是如何实现的,其实这个交互方向,我是从一个前端大佬:前端小付 那里沟通了解到的,我只参考了他的交互思路,实现方式其实完全不同。
方案调研
BPMN:
阿里G6:
g6.antv.antgroup.com/examples/tr…
问题思考:
- 插件体积本身会增加开销。
- 插件集成成本较高。
- 自研流程节点并不需要复杂动画交互、拖拽交互等。
实现方案
效果图
整体思路
通过React手动开发每个节点元素,最终通过线条连线,其实没有任何技术含量,就是html+css实现,比较复杂的就是一些线条距离计算、结构定义等。
事件点击如果按照这种方式来定义,其实是可以实现很多复杂的业务交互逻辑的,从而实现逻辑编排。 除了用React画流程节点以外,我们最终执行事件行为的时候,还需要把它转换成有规律的结构对象,才能让系统按顺序执行每一个节点。
当某一个事件的节点过多时,会出现操作不便,比如画布放不下十几个节点,导致出现滚动条,为了解决这个问题,我引入了react-infinite-viewer
插件,可以让画布移动、放大或者缩小,非常完美的解决了事件流太长的问题。
数据结构定义
类型定义
type NodeType = {
id: string;
type: 'start' | 'end' | 'normal' | 'condition' | 'success' | 'fail';
title: string;
content?: string;
config?: any;
nodeList?: NodeType[];
};
- 开始节点
{
type: 'start'
}
- 普通节点
{
id: '1',
type: 'normal',
title: '删除确认',
content: '显示弹框确认',
config: {}
}
- 分支节点
{
id: 2,
type: 'condition',
title: '分支',
nodeList: []
}
- 结束节点
{
id: 'end',
type: 'end',
title: '开始',
}
- 完整的结构
[
{
"id": "start",
"type": "start",
"title": "开始"
},
{
"id": "63001040",
"type": "normal",
"title": "删除确认",
"content": "确认框",
"config": {
"actionType": "showConfirm",
"actionName": "确认框",
"type": "confirm",
"title": "确认",
"content": "确定要执行此操作吗?",
"okText": "确认",
"cancelText": "取消"
},
"children": []
},
{
"id": "03733450",
"type": "condition",
"title": "",
"content": "行为配置",
"config": {},
"children": [
{
"id": "76742872",
"type": "success",
"children": [
{
"id": "76789323",
"type": "normal",
"title": "调用接口",
"content": "发送请求",
"config": {
"actionType": "request",
"actionName": "发送请求",
"id": "be94339b-72b1-4812-a8f3-fa8ce865234b"
},
"children": []
},
{
"id": "31990605",
"type": "normal",
"title": "刷新列表",
"content": "组件方法",
"config": {
"actionType": "methods",
"actionName": "组件方法",
"target": "MarsTable-d1fbf9m8xpk00",
"method": "reload",
"methodName": "刷新"
},
"children": []
}
],
"title": "成功",
"content": "成功后执行此流程"
},
{
"id": "26162464",
"type": "fail",
"title": "失败",
"content": "失败后执行此流程",
"children": []
}
]
},
{
"id": "end",
"type": "end",
"title": "结束"
}
]
组件定义
// 开始节点
const StartNode = ({ index }: { index: number }) => {
return (
<div className="start-node">
<div className="title">开始</div>
<span className="arrow-line"></span>
<AddNode index={index} id="start" />
</div>
);
};
// 结束节点
const EndNode = () => {
return (
<div className="end-node">
<div className="title">结束</div>
</div>
);
};
// 普通节点
const NormalNode = ({ node, index }: { node: NodeType; index: number }) => {
return (
<div className="normal-node">
<div className="node-info">
<div className="title">{node.title}</div>
<div className="content">{node.content}</div>
</div>
<span className="arrow-line"></span>
<AddNode index={index} id={node.id} />
</div>
);
};
// 条件节点
const ConditionNode = ({ children }: any) => {
return (
<div className="condition-node">
<div className="title">分支</div>
<div className="node-list">{children}</div>
<span className="arrow-line"></span>
</div>
);
};
// 条件节点 - 节点项
const ConditionItem = ({ type, children }: any) => {
return (
<div className="node-item">
<span className={'left-line ' + type}></span>
<span className={'right-line ' + type}></span>
<span className="connect-line"></span>
<div className="normal-container">{children}</div>
</div>
);
};
// 创建节点popover
const AddNode = ({ id, index }: { index: number; id: string }) => {
return (
<span className="add-node-btn">
<span className="add-icon">
<PlusOutlined style={{ fontSize: 16, color: '#fff' }} />
<div className="popover">
<a onClick={() => handleCreateNode('normal', id, index)}>普通节点</a>
<a onClick={() => handleCreateNode('condition', id, index)}>分支节点</a>
</div>
</span>
</span>
);
};
数据渲染
function renderNodeList(nodes: any) {
return nodes.map((node: any, index: number) => {
switch (node.type) {
case 'start':
return <StartNode key={node.id} index={index} />;
case 'end':
return <EndNode key={node.id} />;
case 'normal':
return <NormalNode key={node.id} node={node} index={index} />;
case 'condition':
return (
<ConditionNode key={node.id} title={node.title}>
{node.nodeList.map((item: any, index: number) => {
return (
<ConditionItem key={item.id} type={index === 0 ? 'start' : index == node.nodeList.length - 1 ? 'end' : 'center'}>
{renderNodeList([item])}
{renderNodeList(item.nodeList)}
</ConditionItem>
);
})}
</ConditionNode>
);
default:
return null;
}
});
}
return <div className="node-container">{renderNodeList(list)}</div>;
数据结构转换
通过事件流配置出来的,本身是一个一维数组,如果有条件分支时,会在条件分支对象里面增加子流程。但是前端在执行这些流程节点的时候,最好的方式是转换成伪链表,通过链表结构在结合async
和await
可以保证每个节点的串联执行,也就是前一个执行完成以后,通过next
执行后一个。
一维数组转换为链表
[
{
"actionType": "showConfirm",
"actionName": "确认框",
"type": "confirm",
"title": "确认",
"content": "确定要执行此操作吗?",
"okText": "确认",
"cancelText": "取消"
},
{
"actionType": "request",
"actionName": "发送请求",
"id": "be94339b-72b1-4812-a8f3-fa8ce865234b"
},
{
"actionType": "methods",
"actionName": "组件方法",
"target": "MarsTable-d1fbf9m8xpk00",
"method": "reload",
"methodName": "刷新"
}
]
// 生成链表以后
{
action: {
"actionType": "showConfirm",
"actionName": "确认框",
"type": "confirm",
"title": "确认",
"content": "确定要执行此操作吗?",
"okText": "确认",
"cancelText": "取消"
},
next: {
action: {
"actionType": "request",
"actionName": "发送请求",
"id": "be94339b-72b1-4812-a8f3-fa8ce865234b"
},
next: {
"actionType": "methods",
"actionName": "组件方法",
"target": "MarsTable-d1fbf9m8xpk00",
"method": "reload",
"methodName": "刷新"
}
}
}
嵌套数组转换
如果是带有分支判断的流程节点,相对会复杂一些,它在链表的基础上会做出一些调整,比如next
对象中需要添加success
和fail
对象。
{
"action": {
"actionType": "showConfirm",
"actionName": "确认框",
"type": "confirm",
"title": "确认",
"content": "确定要执行此操作吗?",
"okText": "确认",
"cancelText": "取消"
},
"next": {
"success": {
"action": {
"actionType": "request",
"actionName": "发送请求",
"id": "be94339b-72b1-4812-a8f3-fa8ce865234b"
},
"next": {
"action": {
"actionType": "methods",
"actionName": "组件方法",
"target": "MarsTable-d1fbf9m8xpk00",
"method": "reload",
"methodName": "刷新"
}
}
},
"fail": null
}
}
目前在Mars
系统中,转换这个链表(不是真正意义的LinkedList)结构的函数还是比较复杂的,我这儿暂时不放出来了,以上是整个事件流节点的开发思路。