基于X6开发 JavaScript 蓝图

蓝图是什么

这里的蓝图其实指的是使用图形化的技术来创建代码逻辑,像是虚幻引擎,建模软件 blender 等等都有这种功能,开发可以通过拖放节点,连线来决定代码执行顺序,可以设置参数,蓝图自带基础节点,比如流程类的循环,分支,数学类,判断等等

为什么选择X6

先分析下蓝图最基本的功能(图编辑)

  • 需要一个画布进行节点的放置、缩放管理等
  • 对各种类型的节点定义,拖拽到画布中,复制粘贴等
  • 对节点进行位置更改,框选等
  • 对节点连线,表示逻辑流程以及参数的设置等

如果要完全自己实现功能,需要的工作量还是很大的,所以肯定是要选择开源库的,我个人还是很喜欢 Antv 的,插件化,很容易定制和拓展,所以最后选了 X6 作为技术底座,来进行图编辑相关的功能,这是官网的介绍

X6 是基于 HTML 和 SVG 的图编辑引擎,提供低成本的定制能力和开箱即用的内置扩展,方便我们快速搭建 DAG 图、ER 图、流程图、血缘图等应用。

这样只需要关心事情就变成了

  • 自定义节点格式以及对应图形
  • 连线处理,参数设置
  • 运行逻辑

最后技术选型选了 React + X6

实现

参考虚幻引擎的蓝图:

我们先要定义这个图形,然后区分节点类型,连接桩的布局可以使用 magnetX6 里只要 dom 上带有 magnet 属性,就可以作为边的连接点,参数设置只需要有个类型对应的设置器就行了

初始化 X6 实例

这里初始化需要做的比较多,比如注册插件、注册节点 shape、注册连线的路由,定义合法节点等等

这里说一下缩放调整网格背景,如果不处理,缩小之后网格会比较难看

arduino 复制代码
const gridSize = {
  1: 16,
  0.5: 16 * 2,
  0.33: 16 * 3,
  0.16: 16 * 6,
};
export function updateGridSize(graph: Graph, scale: number) {
  if (scale <= 0.16) {
    graph.setGridSize(gridSize[0.16]);
  } else if (scale <= 0.33) {
    graph.setGridSize(gridSize[0.33]);
  } else if (scale <= 0.5) {
    graph.setGridSize(gridSize[0.5]);
  } else {
    graph.setGridSize(gridSize[1]);
  }
}

const graph: Graph = new Graph({
  // 略
  mousewheel: {
    enabled: true,
    guard(event: WheelEvent) {
      const scale = graph.transform.getScale().sx;
      if (scale >= 1 && event.deltaY < 80) {
        if (event.ctrlKey) {
          return true;
        }
        return false;
      }
      updateGridSize(graph, scale);

      return true;
    },
  },
  scaling: {
    min: 0.13,
    max: 2,
  },
  background: {
    color: '#262626',
  },
  grid: {
    visible: true,
    type: 'doubleMesh',
    size: 16,
    args: [
      {
        color: '#353535', // 主网格线颜色
        thickness: 2, // 主网格线宽度
      },
      {
        color: '#161616', // 次网格线颜色
        thickness: 2, // 次网格线宽度
        factor: 8, // 主次网格线间隔
      },
    ],
  }
})

然后就是设置 allowPort 来判断边和连接点是否能够连接,比如 input 出来的边只能和 output 连接,并且类型需要一致,比如是否都为执行引脚或者是相同数值类型的参数,也可以联动 highlight 来做一些高亮等处理

定义基本图形

X6 也是支持自定义 shape,用的是 React 所以用 @antv/x6-react-shape 插件的 register,所以直接定义一个BaseNode 作为最基本的节点,头部 icon 以及 节点名称,加上内部的执行引脚和参数引脚

ini 复制代码
import { register } from '@antv/x6-react-shape';

export enum ShapeName {
  BASENODE = 'base-node',
}
function BaseNode(props) {
  const data = props.node.getData();
  const nodeWrapRef = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    if (nodeWrapRef.current) {
      const boundRect = nodeWrapRef.current.getBoundingClientRect();
      props.node.setSize(boundRect.width, boundRect.height);
    }
  }, []);

  useLayoutEffect(() => {
    const pos = props.node.getPosition();
    props.node.setPosition(pos.x + 0.000000001, pos.y  + 0.000000001);
  }, []);

  return (
    <div className="custom-base-node" ref={nodeWrapRef}>
      <div className="custom-base-node-top">{data.name}</div>
      <div className="custom-base-node-content"></div>
    </div>
  );
}
register({
  shape: ShapeName.BASENODE,
  component: BaseNode,
});

这里第一个 useLayoutEffect 想要自适应节点的大小,所以设置了一次size,第二个则是因为 graph.fromJSON 渲染完发现边会出现错乱的情况,暂时用 setPosition hack了一下,还没去找真正的原因

这两点有更好的做法大家可以说一下

定义蓝图节点分类

因为流程总是由一个事件触发开始的,比如鼠标、键盘、或者是观察的事件等,然后根据执行引脚执行相关的流程,一般也是函数体,所以先枚举一个 NodeType 以及对应的 icon 以及 颜色 来区分下类型

vbnet 复制代码
enum NodeType {
  FUNCTION = 'Function',
  EVENT = 'Event',
}
export const NodeTypeMeta = {
  [NodeType.FUNCTION]: {
    icon: SVGIcon.function,
    color: '64, 109, 247',
  },
  [NodeType.EVENT]: {
    icon: SVGIcon.event,
    color: '180, 25, 25',
  }
};

使用 JSON 描述节点

接下来就要想想如何使用 JSON 描述一个蓝图节点, X6 是推荐在 data 中去携带业务数据,并且提供 getData setData 等相关联的操作,所以我们额外的属性就放在 data 里即可,我们先简单定义一下需要的类型

typescript 复制代码
export interface Pin {
  // 引脚类型
  type?: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'exec';
  // 设置器
  setter?: string;
  // 参数显示名称
  displayName?: string;
  // 参数名称
  name?: string;
  // 存储的类型 比如是 object 或者是 array
  fields?: Pin | Pin[];
  // 参数值
  value?: unknown;
  [key: string]: unknown;
}
export interface NodeDataItem {
  //用于判断触发了哪一个节点
  id: string;
  //节点名称
  name: string;
  //节点类型
  type: NodeType;
  //节点的shape 默认是 base-node
  nodeShape?: ShapeName;
  //描述
  description?: string;
  //执行输入
  execInput?: Pin[];
  //执行输出
  execOutput?: Pin[];
  //参数输入
  input?: Pin[];
  //参数输出
  output?: Pin[];
  [key: string]: unknown;
}

假设是一个按钮 ,需要一个点击事件的节点描述, 让用户只需要这么写(这里 event 的 fields 参数太多, 就不写了):

json 复制代码
{
  "name": "onClick",
  "displayName": "点击",
  "output": [
    {
      "name": "event",
      "displayName": "event",
      "type": "object"
    }
  ]
}

然后在 createNode 的时候添加一些默认的字段,比如 shape 直接用 ShapeName.BaseNode 和 type 为 NodeType.Event等字段,顺便存一个 originalData 来方便复制粘贴

kotlin 复制代码
// data 为上面的 json 处理过的
const node = graph.createNode({
  shape: data.nodeShape || ShapeName.BASENODE,
  type: data.type,
  width: 300,
  height: 150,
  ...data.defaultX6Setting,
  data: {
    ...data,
    originalData: cloneDeep(data),
  },
});

我们看看添加节点后展示的样子

如何执行流程

可以通过 X6toJSON 把图的 Cells 全部存储起来,然后在代码运行的时候,通过执行边进行寻找下一个执行节点,参数也是通过边进行寻找,如果没有边则直接读取当前的 value,就可以了

效果展示

图相关的操作逻辑 X6 已经做的非常好了,所以只需要额外做一点事情就可以完成我们所需要的业务功能,这是最后在产品中集成的效果展示 点击链接查看.mp4

最后

在这个示例中的图表其实也是用的 AntvG2,所以非常感谢 Antv 让我节省了很多时间,作为开源的使用者,所以在使用的过程中将遇到的问题解决后也会回馈到社区,在 issue 里或者群聊中解答疑问,虽然都是微不足道的小问题,但也算是做到从开源中来,到开源中去了

希望 Antv 越来越好,能够帮助到越来越多的人

相关推荐
2分钟速写快排2 分钟前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby606135 分钟前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角2 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!2 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰2 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi3 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen4 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马5 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu5 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss
IT_陈寒5 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端