Mars低代码中逻辑编排的设计思路

1,056 阅读5分钟

背景

在上一篇文章中给大家分享了个人开发的Mars零代码平台,其中它的事件流是基于流程节点的形式开发的,非常有意思,相比于传统的低代码事件交互有很多优点。那本篇文章想介绍一下,Mars中的事件流交互是如何实现的,其实这个交互方向,我是从一个前端大佬:前端小付 那里沟通了解到的,我只参考了他的交互思路,实现方式其实完全不同。

方案调研

BPMN:

demo.bpmn.io/s/start

阿里G6:

g6.antv.antgroup.com/examples/tr…

问题思考:

  • 插件体积本身会增加开销。
  • 插件集成成本较高。
  • 自研流程节点并不需要复杂动画交互、拖拽交互等。

实现方案

效果图

image.png

image.png

整体思路

通过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[];
};
  1. 开始节点
{
    type: 'start'
}
  1. 普通节点
{
  id: '1',
  type: 'normal',
  title: '删除确认',
  content: '显示弹框确认',
  config: {}
}
  1. 分支节点
{
  id: 2,
  type: 'condition',
  title: '分支',
  nodeList: []
}
  1. 结束节点
{
  id: 'end',
  type: 'end',
  title: '开始',
}
  1. 完整的结构
[
    {
        "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>;
数据结构转换

通过事件流配置出来的,本身是一个一维数组,如果有条件分支时,会在条件分支对象里面增加子流程。但是前端在执行这些流程节点的时候,最好的方式是转换成伪链表,通过链表结构在结合asyncawait可以保证每个节点的串联执行,也就是前一个执行完成以后,通过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对象中需要添加successfail对象。

{
    "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)结构的函数还是比较复杂的,我这儿暂时不放出来了,以上是整个事件流节点的开发思路。