怎样给iMove开发一个Debug插件——可视化逻辑编排的Debug实现

721 阅读8分钟

阿里开源了一个可视化逻辑编排的实现——iMove,可以使用流程可视化编排、逻辑组件配置的方式,进行业务流程的编排实现。

iMove

iMove可视化逻辑编排

什么是可视化逻辑编排?

我们好多人在写代码之前,都有画流程图、ER图、UML图的习惯,通过流程可视化的方法、把我们要表达的业务逻辑梳理清楚,而我们最终实现的代码逻辑、其实也相当于一个流程图。而计算机科学的预备知识之一,就是类似下面的“流程图”:

计算机科学精粹-预备知识流程图.png

计算机科学预备知识之一——流程图

说到可视化编程,Google提供了一个叫做Blockly的可视化编程语言,通过类似积木组装的隐喻,开发者通过可视化组块的方式进行编程。麻省理工学院(MIT)基于Google的Blockly进行了Scratch的设计开发,方便少儿进行编程教育。

Scratch

而在BPM(Business Process Management,业务流程管理)领域,往往整个工作流程一般都是通过可视化配置的方式进行业务流程的搭建和编辑。

BPM

BPM业务流程编排

总体来说,iMove算是一种F2C(Flow 2 Code)的实现,即通过流程可视化编排来产生代码。多关于iMove的原理和相关文章,请参见其语雀博客

关于iMove的逻辑可视化编排,我们需要知道是它是由“逻辑节点”和连接每对逻辑节点的“边”所构成的“图数据结构”:

  1. 节点:每个“逻辑节点”,本质上是一个JavaScript函数,提供某种API服务;逻辑节点的属性配置,包括函数定义、相关依赖、组件参数配置等等内容;
  2. 连线:两个“逻辑节点”之间可以连线,通过在各个边的箭头指向关系,确定流程执行时的上下游关系;

怎样给iMove开发一个Debug插件

iMove本身提供了一个插件机制plugin-store,可以通过插件的形式、在逻辑执行的各个生命周期(enterNode/leaveNode)中添加自定义逻辑。比如iMove官方有一个MockPlugin,提供了在线使用Mock数据、执行单个节点逻辑的功能。

我们接下来给iMove开发一个Debug插件,提供可视化逻辑编排的在线Debug的功能,大概的示意图如下:

iMove-Debug.png

可视化逻辑编排在线Debug

iMove-Debug.gif

第0步 Debug插件的原型

如果你看过iMove的这篇《4. iMove 基于 X6 + form-render 背后的思考》博客文章的话,会知道iMove的流程图绘制部分,使用了阿里开源的图编辑引擎——AntV X6

通过查看Antv X6官方的示例,我们能发现一个“人工智能建模 DAG 图”的示例,它本身实现了一个状态驱动的人工智能执行流程——通过一步一步的状态变化,展示了人工智能的执行流程。我们要实现的Debug插件的原型,就是来源于这个人工智能建模DAG图示例

x6-Dag.png

具体示例请参见x6.antv.vision/zh/examples… :我们可以从示例中看到,随着DAG图执行状态的一次又一次地变化,在流程图的UI上、该状态被同步进行了展示——这正是我们想要的Debug效果。

另外,如果你看过bpmn.js的一个BPMN Token Simulation实现,就能了解到——通过这种逐个流程节点地执行,新手更容易上手这种流程图开发,另一方面也方便对自己刚刚自己做的流程图进行简单地测试:

根据👆🏻上面的功能原型,我们要简单地实现如下所示的一个iMove Debug Plugin:

iMove-Debug.png

为了完成整个功能,我们可以把整个功能实现拆成如下几个大的步骤:

  1. 红色断点实现:iMove流程图“节点”改造,允许添加“红色断点”等可视化元素,以便允许用户在节点上添加断点;
  2. Debug插件实现:使用和改造iMove本身的插件机制,添加一个plugin-store的Debug Plugin插件,实现逻辑执行的各个生命周期控制——在断点所在节点“enterNode”生命周期时、阻断流程执行,记录当前节点执行之前的payload, pipe, context, config等等参数快照;并在节点的“leaveNode”生命周期时,记录当前节点执行后的payload, pipe, context, config等等参数的快照;
  3. 参数状态展示:将当前节点的payload, pipe, context, config等等参数展示出来。

接下来,我们按照上述3个步骤,实现一个简单地查看参数状态的Debug插件:

第1步:红色断点的实现

iMove流程图的节点,是使用X6默认的SVG绘制的:

import { Shape } from '@antv/x6';

const schema = {
  base: Shape.Circle,
  shape: 'imove-start',
  width: 60,
  height: 60,
  label: '开始',
  // ... ...
}

export default schema;

而我们需要支持在节点上添加红色的 断点 ,需要允许用户在断点上进行简单地Toggle状态交互(设置断点、取消断点),而AntV X6的节点是支持使用React组件开发的,所以为了能够使用我顺手的技术栈进行更多功能的开发,需要改造一下节点的实现、使用React组件进行节点开发:

Graph.registerNode('imove-start', schema);
Graph.registerReactComponent('imove-start', (node: any) => {
return <StatusView node={node} />;
});

通过Graph.registerNodeGraph.registerReactComponent这两个API,我们可以创建自定义的、使用React组件开发的流程图节点。而在节点的React组件中,我们就可以为所欲为地添加各种React组件,比如 红色断点

const StatusView = function ({ node }: any) {
  const status = useNodeStatus(node);
  // ... ...
  return (<div className={styles.statusWrapper}>
    // ... ...
    <DebugPoint node={node} status={status} />
    {status?.jobStatus && statusIcon}
  </div>);
};

/** 这里是红色断点的实现 */
const DebugPoint = function ({ node, status }: any) {
  return (<div
    className={classNames(styles.debugPoint, {
      debugging: status?.isDebuggingPointOn })}
    onClick={status?.onToggleDebuggingPoint}
  >
    <StopFilled style={{ color: '#ff4d4f' }} />
  </div>);
}

我们通过定义一个useNodeStatus的Hook,来确定当前React Component节点的显示状态,比如这个红色断点<DebugPoint>组件的实现,就是通过当前节点是否已经设置为断点状态(isDebuggingPointOn),进行断点状态的显示,而且可以通过这个组件、进行断点状态的开和关(onToggleDebuggingPoint)。

如果需要新增节点的状态,比如节点执行成功显示对号✅、执行失败显示叉号❎,都可以直接在React Component节点中进行新React组件的开发:

x6-Dag.png

第2步:iMove Debug插件的实现

iMove的plugin-store插件,提供了iMove流程节点执行时的3个生命周期的实现('ctxCreated', 'enterNode', 'leaveNode'):

  1. ctxCreated生命周期:在ctx对象创建时调用,这个ctx对象包含iMove逻辑节点执行的4个参数(payload, pipe, context, config),我们这个Debug Plugin的实现,其实就是查看ctx对象的这4个参数的状态;
  2. enterNode生命周期:在当前逻辑节点的节点函数 执行前 触发;
  3. leaveNode生命周期:在当前逻辑节点的节点函数 执行后 触发;

我们要实现的Debug Plugin,需要重点关注enterNodeleaveNode这两个生命周期,整个Debug Plugin的大概实现如下:

export const debugPlugin = () => {
  return { enterNode, leaveNode };
};
// 节点执行前
async function enterNode(ctx: any) {
  // 1. 获取当前节点ctx的payload, pipe, context, config状态快照
  // 2. 将当前节点ctx的状态快照,在流程图中展示出来
}
// 节点执行后
async function leaveNode(ctx: any) {
  // 1. 获取当前节点ctx的payload, pipe, context, config状态快照
  // 2. 将当前节点ctx的状态快照,在流程图中展示出来
}

具体实现如下——主要意图是从iMove的ctx对象对象中获取payload, pipe, context, config状态快照,然后将状态快照信息、通过React组件展示出来:

export const debugPlugin = () => {
  return { enterNode, leaveNode };
};

async function enterNode(ctx: any) {
  nodeStatusSnapshot[ctx.curNode.id] = ctx.getNodeDebuggerParam();
  if (currentLogic?.isDebugging && debuggerBreakPointsMap[ctx.curNode.id]) {
    return new Promise((resolve) => {
      breakPoints.push({
        nodeId: ctx.curNode.id,
        resolve,
        context: ctx,
      });
      // 1. 获取当前节点ctx的payload, pipe, context, config状态快照
      const newState = getStatus();
      const { instStatus, execInfo } = newState.data;
      set(instStatus, ctx.curNode.id, 'running');
      set(execInfo, `${ctx.curNode.id}.jobStatus`, 'running');
      debuggingState.statusRes = newState;
      // 2. 将当前节点ctx的状态快照,在流程图中展示出来
      executionStatus$?.next(newState.data);
    });
  }
}

async function leaveNode(ctx: any) {
  if (currentLogic?.isDebugging) {
    // 1. 获取当前节点ctx的payload, pipe, context, config状态快照
    const newState = getStatus();
    const { instStatus, execInfo } = newState.data;
    set(instStatus, ctx.curNode.id, ctx.success ? 'success' : 'fail');
    set(execInfo, `${ctx.curNode.id}.jobStatus`, ctx.success ? 'success' : 'fail');
    debuggingState.statusRes = newState;
    // 2. 将当前节点ctx的状态快照,在流程图中展示出来
    executionStatus$?.next(newState.data);
  }
}

就这样,在当前流程节点执行之前(即enterNode生命周期),可以看到上一个节点传过来的payload, pipe, context, config等状态快照;而在当前节点执行之后(即leaveNode生命周期),可以保存当前节点执行完成之后的payload, pipe, context, config等等状态快照,方便在下一个节点进行查看。

第3步:参数状态展示的实现

这个iMove Debug插件的主要意图是从iMove的ctx对象对象中获取payload, pipe, context, config状态快照,然后将状态快照信息、通过React组件展示出来:

iMove-Debug.png

这其实就是把ctx参数对象的键值对都显示出来,我这里就直接参考了React DevTools的KeyValue等等的实现,将payload, pipe, context, config键值对显示出来。

import * as React from 'react';
import styles from './StateInspector.less';
import KeyValue from './KeyValue';

export default function StateInspector(state: { payload: any; pipe: any; context: any; config: any; }) {
  const entries = state != null ? Object.entries(state) : null;

  const isEmpty = entries === null || entries.length === 0;
  if (isEmpty) {
    return null;
  } else {
    return (
      <div className={styles.InspectedElementTree}>
        {isEmpty && <div className={styles.Empty}>暂无</div>}
        {!isEmpty &&
          entries.map(([name, value]) => (
            <KeyValue key={name} depth={1} hidden={false} name={name} path={[name]} value={value} />
          ))}
      </div>
    );
  }
}

以上就是iMove的Debug插件开发的主要思路,希望对大家在开发可视化编排的Debug功能时,提供一些启发和简单的实现思路。