基于 AntvX6 的流程编排系统搭建实践
一、 引言
众所周知,前端的流程化配置与框架升级,一直以来都是所有前端开发所需要经历的一环。恰巧最近有项目要进行整合重构,一想这不就有上手案例了,干脆直接拥抱新技术(当然也不算新了)。
这次的开发中,主要涉及节点、转换插件的流程和图表编排,所以自然是少不了使用流程框架,这里简单列举几个市场常见的框架:Butterfly(阿里)、JointJS、G6、X6
。最后综合考虑选型更适用react的开发方式的 X6
作为项目解决方案。另外由于是从零构建新系统,这里我使用的是 Procomponents
来搭建基础页面及表单框架,这个框架先不展开讨论,因为里面也有许多优点和坑,留到下次我再做分享。本文的方向主要是对 X6
在前端业务中的使用实践和问题进行展开。
二、 X6技术分析
2.1 X6 简介与 Bpmn.js 选型对比
在第一次接触X6的时候,我就被他可视化、可编排的能力所吸引,同时他还具备针对各种业务场景的可拓展性,具体的功能树展示如下,感兴趣的话,大家可以根据自己感兴趣的方向去尝试体验一下。
那么说了半天X6具体是什么呢,官方给出的介绍是这样的:
X6 是基于 HTML 和 SVG 的图编辑引擎,提供低成本的定制能力和开箱即用的内置扩展,方便我们快速搭建 DAG 图、ER 图、流程图、血缘图等应用。
这里也简单介绍下 bpmn.js
BPMN(业务流程管理)是由 BPMN组织 发布一种业务流程建模和标记的标准协议。它提供了一种符号系统,用于描述业务流程中的各种活动、事件、网关和流程之间的关系。
作为技术栈的选型之一,也作为选型思考的例子,这里分析了下两者的异同点:
相同点:
图形化建模展示: bpmn.js 和 X6都是用于图形化建模的工具,它们都注重通过清晰、直观的方式实现可
视化功能,让用户更好的创建和展示业务流程或图形界面,从而加速用户的理解和相互沟通设计内容的效率。
支持事件交互: 都支持一定程度的交互功能,如节点之间的连接、事件的触发等。
定制化 :两者都允许用户进行定制化操作来实现特定的业务场景。
不同点:
概念和业务方向:
bmpn.js :bpmn.js是一款由 BPMN.io 组织基于 BPMN 2.0 开发的一款前端开发工具集。它更
适用于描、解释和分析业务流程,例如像OA工单系统的流程配置场景,它能够更好的让技术与非技术
人员交流和理解。
X6: X6基于 HTML 和 SVG 的图编辑引擎,它基于HTML5 Canvas和SVG技术,提供了丰富的功能
来创建各种类型的图形、流程图和图表。它适用于创建和定制图形化界面和画布工具,特别是在需要
高度定制和灵活性的应用程序中,拥有相对bpmn.js 更高的定制化开发能力。
相对而言, X6 更友好兼容react写法,在处理复杂流程时相对 bpmn.js 更好理解,同时它在图
表节点的渲染和功能的自定义实现上更为突出。另外 还有一个重要的优点,就是 X6 团队在
2.x版本做了相当多的插件分包处理,每一个依赖包都可以单独引入使用,实现了按需加载,
所以综合对比后,我选择了X6来实现任务流程的可视化工作。
三、 项目实践
3.1 编排设计与踩坑
3.1.1 流程设计思路
下面具体讲一下如何设计和开发的,在说这一环节的时候,我希望大家了解一个概念:DnD ,字面含义就是:Drag and Drop(拖拽与放置)。在三方库中, DnD集成了store 、Backends(负责注册拖拽相关的事件)、teardown(销毁事件)等一系列操作,了解完概念那我们再往下看。
根据对业务需求的分析,流程模块主要是分三块区域,思考的页面设计如下:
节点配置:
这里配置画布需要的节点与转换插件,X6支持最基础的例如:圆形、菱形、方形等图形节点,同时
也可以支持图片、自定义节点;
节点 用来定义数据源与数据目标,同时节点需要拥有配置的能力;
转换插件 用来处理上下游不同数据类型的对应数据转换,目前包含:
Copy 对数据源字段的复制,扩充
FilterRowKind 数据库增量同步时用于对特定的sql类型进行过滤
Replace 对数据源字段的替换
SQL 在传输过程中执行 SQL数据转换操作,如concat
内容编排:
整体的画布区域Graph,节点、连接桩、群组、圈选、拖拽等操作都在这里进行,也可以对画布做定制化开发,同时画布也集成了快捷操作工具,方便用户使用;
节点逻辑管理:
这里指的是节点或插件所需的配置管理,包含对单个节点、连线的基础数据、转化方法和自定义属性的存储转化,是整个流程的核心;
整体的设计方案有了,那么接下来就是开发阶段,具体怎么去实现这一系列的交互呢?我们一步步地来展开去讲。
3.1.2 自定义节点定义与实现
为了拥抱新技术,这里我用的是X6官方最新的 2.x 版本,相对于 1.x 繁重的配置和使用方式,官方做了代码解耦,插件单独拆包安装,做到可插拔,更多的改动可以看 2.x版本升级文档 详细了解,这里不多赘述。
当我刚接触X6的时候我是一头雾水的,因为去看了官方文档中最贴合业务的案例,它长这样:
图中虽然具有基础的节点配置,但是与UI和业务的要求还是有一定差距,所以第一反应就想到了自定义节点,就直接去搜是否有自定义节点的react写法。在这里不得不吐槽一下X6的文档,虽然api跟基础用法很齐全,但是对小白来说阅读起来相当吃力,缺少单个方法的运行案例,也存在很多无法查找到的问题,你只能自己去看底层源码。
官方为react节点单独提供了 @antv/x6-react-shape
用于注册和渲染DOM,这里需要注意x6的
版本必须和 @antv/x6-react-shape
保持在一个大版本,且插件最新版本 仅支持react18
,低于18的版本需要将插件锁定在 2.0.8
。
这里业务还需要增加节点的tooltip提示,但是由于是同一节点的配置,侧边栏中会存在提示错位的问题,这里我是通过定位元素父元素来处理浮窗绑定的。位置,如果有更好的方式也可以互相交流,具体实现如下:
import { Tooltip } from 'antd';
import { register } from '@antv/x6-react-shape';
...
// 自定义react组件节点
const NodeComponent = ({ ...arg }) => {
const { node, graph } = arg;
const { label, icon, tip } = node.getData();
const { sourceType, sourceInstance, sinkType, sinkInstance, dbUri } = node.attr('nodeData') || {};
const isOriginCell = ['source', 'sink'].includes(node.attr('action'));
const selectCells = graph ? graph.getSelectedCells() : [];
const isSelected = selectCells.some(item => item.id === node.id);
const renderTip =
isOriginCell && (sourceType || sinkType) ? (
<div>
<div>节点信息</div>
<div>
数据库类型:
<span>{dbTypeEnums.find(v => v.value === (sourceType || sinkType))?.label || '-'}</span>
</div>
<div>
资源名称:<span>{sourceInstance || sinkInstance}</span>
</div>
</div>
) : (
tip
);
const isInStencil = trigger => {
const parentContainer = trigger.parentNode.parentNode.parentNode.parentNode;
const checkInSide = parentContainer.classList.contains('x6-node-immovable'); // 存在该class则在stencil内(比较Low的方法)
return checkInSide ? document.body : trigger;
};
return (
<Tooltip
className="x6-node-item"
placement="top"
title={renderTip}
fresh
getPopupContainer={trigger => isInStencil(trigger)}
>
<div
className="react-node w-170 h-36 flex-left bg-white pointer"
style={{ border: '1px solid #E8E9ED' }}
>
<span className="w-3 h-100 bg-408BFF" />
<ZaIcon type={icon} className="f-s-16-imp m-l-10 col-8AA08E-imp" />
<span className="m-l-8">{label}</span>
{(sourceType || sinkType) && (
<ZaIcon
type={iconType[sourceType || sinkType]}
className="f-s-16-imp m-l-12 col-8AA08E-imp"
/>
)}
</div>
</Tooltip>
);
};
当我们实现了自定义节点的功能,接下来就是怎么渲染到整个画布的节点配置中,这里就涉及到了一个关键点:节点注册。官方提供了一个包@antv/x6-react-shape
,这个依赖包集成了包括 node、view、registry、portal等等不同工具类的相关方法,这里我们使用registry中的 register
方法,底层ts定义如下:
import React from 'react';
import { Graph, Node } from '@antv/x6';
export type ReactShapeConfig = Node.Properties & {
shape: string;
component: React.ComponentType<{
node: Node;
graph: Graph;
}>;
effect?: (keyof Node.Properties)[];
inherit?: string;
};
export declare const shapeMaps: Record<string, {
component: React.ComponentType<{
node: Node;
graph: Graph;
}>;
effect?: (keyof Node.Properties)[];
}>;
export declare function register(config: ReactShapeConfig): void;
这里我们注册一个名为 custom-react-node
的节点名,对属性 component 配置我们定义好的NodeComponent
即可,下面的ports配置了该节点在 left、right 方向上的连接点组。
import React from 'react';
import { register } from '@antv/x6-react-shape';
...
// 自定义节点注册
register({
shape: 'custom-react-node',
width: 170,
height: 36,
component: NodeComponent,
ports: {
groups: {
right: {
position: 'right',
attrs: {
circle: {
r: 5,
magnet: true,
stroke: '#006aff',
strokeWidth: 1,
fill: '#fff',
style: {
visibility: 'hidden',
},
},
},
},
left: {
position: 'left',
attrs: {
circle: {
r: 5,
magnet: true,
stroke: '#006aff',
strokeWidth: 1,
fill: '#fff',
style: {
visibility: 'hidden',
},
},
},
},
},
items: [
{
group: 'right',
},
{
group: 'left',
},
],
},
});
当我们完成了注册后会发现,此时节点还没有出现在侧边节点区域,这里我们还需要做的一步,就是在X6的Graph初始化时进行初始化赋值,这里又涉及到一个注册方法 Stencil
。
Stencil 是在 Dnd 基础上的进一步封装,提供了一个类似侧边栏的 UI 组件,并支持分组、折叠、搜索等能力。
上面提到在2.x版本中,X6将很多沉重的方法都做了抽离,在使用时,我们可以根据自己的情
况进行按需引入。这里我引入了 @antv/x6-plugin-stencil 依赖包, 先实例化一个stencil配置,
再通过 this.stencil.load(…) 的注册方法,将定义好的节点配置 r1、r2、t1、t2 加载到 Stencil
中:
import { Stencil } from '@antv/x6-plugin-stencil';
import './shape'; // 这里是上述的注册定义方法,做了代码拆分
...
// 侧边栏
static initStencil() {
this.stencil = new Stencil({
target: this.graph, // 指定画布
title: '数据源/数据目标', // 标题
stencilGraphWidth: 210, // 侧边栏宽度
// search: { rect: false }, // 搜索
groups: [
// 分组
{
name: 'basic',
title: '数据源/数据目标',
graphHeight: 120,
layoutOptions: {
columns: 1,
},
},
{
name: 'transforms',
title: '转换插件',
layoutOptions: {
columns: 1,
},
graphHeight: 400,
},
],
layoutOptions: {
// 布局
columns: 1,
columnWidth: 180,
rowHeight: 50,
},
});
const stencilContainer = document.querySelector('#stencil');
stencilContainer?.appendChild(this.stencil.container);
}
...
// 初始化节点
static initShape() {
const { graph } = this;
const r1 = graph.createNode({
shape: 'custom-react-node',
width: 170,
height: 36,
attrs: {
action: 'source',
body: {
rx: 2,
ry: 2,
},
text: {
textWrap: {
text: '数据源',
},
},
},
data: {
label: '数据源',
icon: 'icon-Source',
// tip: 'Source',
},
});
...
const t1 = graph.createNode({
shape: 'custom-react-node',
width: 170,
height: 36,
attrs: {
action: 'transForm',
transformType: 'Copy',
text: {
textWrap: {
text: 'Copy',
},
},
},
data: {
label: 'Copy',
icon: 'icon-copy',
tip: '对数据源字段的复制,扩充',
},
});
const t2 = graph.createNode({
shape: 'custom-react-node',
width: 170,
height: 36,
attrs: {
action: 'transForm',
transformType: 'Replace',
text: {
textWrap: {
text: 'Replace',
},
},
},
data: {
label: 'Replace',
icon: 'icon-Replace',
tip: '对数据源字段的替换',
},
});
...
this.stencil.load([r1, r2], 'basic'); // 指定加载的区域name,这里是节点
this.stencil.load([t1, t2], 'transforms'); // 指定加载的区域name,这里是插件
}
经过上述的配置操作,我们就可以在画布初始化后实现自定义的侧边节点配置栏了。
3.1.3 注册事件和自定义事件
当然有人可能会问:如果节点一直要拖拽那不是很麻烦吗,画布就没有快速操作的方法吗?
其实这个不用担心,X6还集成了各种事件的交互、订阅,甚至可以绑定Mac、Windows的快
捷键,做到 一键回退、恢复、圈选复制、粘贴、右键菜单 等快捷操作,另外它也支持 自定义
事件 ,下面举例:
// 初始化事件
static initEvent() {
const { graph } = this;
const container = document.getElementById('container');
/**
* 监听节点折叠事件
*/
graph.on('node:collapse', ({ node, e }) => {
e.stopPropagation();
node.toggleCollapse();
const collapsed = node.isCollapsed();
const cells = node.getDescendants();
cells.forEach(n => {
if (collapsed) {
n.hide();
} else {
n.show();
}
});
});
// 回退事件
graph.bindKey(['meta+z', 'ctrl+z'], () => {
graph.undo();
return false;
});
// 画布居中
graph.bindKey(['meta+f', 'ctrl+f'], () => {
graph.fitToContent();
return false;
});
// 重做事件
graph.bindKey(['meta+shift+z', 'ctrl+y'], () => {
graph.redo();
return false;
});
/**
* 自定义事件,绑定键盘delete键,mac为ctrl+delete
*/
graph.bindKey('delete', () => {
const cells = graph.getSelectedCells();
if (cells.length) {
graph.removeCells(cells);
}
});
/**
* 绑定windows键盘的删除
*/
graph.bindKey('backspace', () => {
const cells = graph.getSelectedCells();
if (cells.length) {
graph.removeCells(cells);
}
});
}
完成了上面的节点操作和快捷键的事件绑定后,我们通过new Graph(...)
来配置所有需要的节点注册、工具类方法,最后一步将graph初始化后,我们就实现了拖拽节点、快捷操作的功能。
这里需要注意的一点是,圈选、键盘事件、History一类的方法我们需要单独引入依赖
,参考
如下:
import { Graph, Shape } from '@antv/x6';
import { Selection } from '@antv/x6-plugin-selection';
import { Snapline } from '@antv/x6-plugin-snapline';
import { Keyboard } from '@antv/x6-plugin-keyboard';
import { Clipboard } from '@antv/x6-plugin-clipboard';
import { History } from '@antv/x6-plugin-history';
...
static init() {
this.graph = new Graph({
container: document.getElementById('container'), // 画布容器
// shift 平移
panning: {
enabled: true, // 画布是否可以拖动
eventTypes: ['leftMouseDown'],
modifiers: 'shift', // 按住shift 可以平移
},
grid: { visible: true }, // 网格
// 配置全局的连线规则
connecting: {
anchor: 'center', // 当连接到节点时,通过 anchor 来指定被连接的节点的锚点,默认值为 center。
connectionPoint: 'anchor', // 指定连接点,默认值为 boundary
allowBlank: false, // 是否允许连接到画布空白位置的点
highlight: true, // 拖动边时,是否高亮显示所有可用的连接桩或节点
snap: true, // 当 snap 设置为 true 时连线的过程中距离节点或者连接桩 50px 时会触发自动吸附
// 连接的过程中创建新的边
createEdge() {
return new Shape.Edge({
attrs: {
line: {
stroke: '#5F95FF',
strokeWidth: 1,
targetMarker: {
name: 'classic',
size: 8,
},
},
},
router: {
name: 'manhattan',
},
zIndex: 0,
});
},
// 在移动边的时候判断连接是否有效
validateConnection({ sourceView, targetView, sourceMagnet, targetMagnet }) {
if (sourceView === targetView) {
return false;
}
if (!sourceMagnet) {
return false;
}
if (!targetMagnet) {
return false;
}
return true;
},
},
// 可以通过 highlighting 选项来指定触发某种交互时的高亮样式
highlighting: {
magnetAvailable: {
name: 'stroke',
args: {
padding: 14,
attrs: {
strokeWidth: 4,
stroke: 'rgba(223,234,255)',
},
},
},
},
snapline: true, // 启动对齐线
history: true, // 启用撤销/重做
// 启用剪切板
clipboard: {
enabled: true,
},
// 启用键盘快捷键
keyboard: {
enabled: true,
},
// 通过embedding可以将一个节点拖动到另一个节点中,使其成为另一节点的子节点
embedding: {
enabled: true,
findParent({ node }) {
const bbox = node.getBBox();
return this.getNodes().filter(node => {
// 只有 data.parent 为 true 的节点才是父节点
const data = node.getData();
if (data && data.parent) {
const targetBBox = node.getBBox();
return bbox.isIntersectWithRect(targetBBox);
}
return false;
});
},
},
});
this.graph
.use(
// 激活键盘事件
new Keyboard({
enabled: true,
}),
)
.use(
// 激活重做事件
new History({
enabled: true,
}),
)
.use(
new Selection({
className: 'flow-select-item',
rubberband: true,
showNodeSelectionBox: true,
}),
)
.use(new Keyboard())
.use(new Clipboard())
.use(new History());
this.initStencil();
this.initGraphShape();
this.initShape();
this.initEvent();
return this.graph;
}
3.2 数据流处理
3.2.1 数据节点及插件配置能力实现
在完成了重要的拖拽功能的基础上,我们还需要给节点添加数据的配置能力,这里对业务需求认真分析后,我遇到了几个主要问题,前两个问题是:
1. X6怎么实现监听节点的点击、双击触发例如高亮、弹窗操作?
2. 怎么能对指定节点进行数据配置?
带着这些问题,我翻阅了官方文档并找到了对应解法:
这里通过监听 cell:dblclick
事件,双击弹出对应节点弹窗,因为单击还有选中高亮的业务功能,这样来解决第一个问题:
useEffect(() => {
const graph = FlowGraph.init(); // 初始化流程图
setIsReady(true);
graph.on('cell:dblclick', ({ cell }) => {
setType(cell.isNode() ? CONFIG_TYPE.NODE : CONFIG_TYPE.EDGE); // 判断点击的是节点还是边
setCellId(cell.id); // 保存节点ID
const cellAction = cell.attr('action');
typeof cell.attr('action') === 'object' &&
cell.attr('action', Object.values(cellAction).join(''));
typeof cell.attr('transformType') === 'object' &&
cell.attr('transformType', Object.values(cell.attr('transformType')).join(''));
cell.isNode() && changeDrawerType(cell.attr('action'), { ...cell.attr(), id: cell.id });
});
...
}, []);
const changeDrawerType = (choose, record) => {
const { mixin_openZaDrawer } = props;
mixin_openZaDrawer(choose, drawerContent(record)[choose]);
};
...
// 弹窗页面配置
const drawerContent = row => ({
...
transForm: {
title: 'transform配置',
footer: null,
layout: 'horizontal',
labelCol: { span: 3 },
wrapperCol: { span: 15 },
// labelAlign: 'left',
grid: true,
form,
autoFocusFirstInput: true,
className: 'form-base',
drawerProps: {
width: 1300,
destroyOnClose: true,
onClose: () => changeDrawerType('none'),
bodyStyle: { paddingTop: 20, paddingBottom: 20 },
zIndex: 1100,
},
submitter: {
searchConfig: {
submitText: '确认',
resetText: '取消',
},
},
onFinish: values => onHandleSaveTransform(values, row?.id),
renderContent: () => {
const basicData = parentFormRef?.current[0]?.current?.getFieldsValue();
return <FlowTransform cellData={row} form={form} basicFormInfo={basicData} />;
},
},
});
在X6中,画布的核心就是 cell 节点
,通过配置 cell.attr
属性数据添加自定义属性来做个性
化操作。这里我们定义了画布节点的 action
判断是否为转换插件节点,同时也定义了transformType
判断为是哪一类插件,从而来唤起对应配置弹窗,这样就解决了第二个问题。
3.2.2 数据存储回显和校验拦截
上述代码已经实现了基础的单点配置功能,然而最关键的问题来了,那就是:怎么将配置数据保存到对应节点/边,对接后端复杂的数据结构又如何处理?
这块说实话也琢磨了不少时间,最后发现其实同样的是对cell属性进行设置,上面的弹窗我们定义了 onFinish 做数据的表单提交处理,实现如下:
// 表单的数据保存
const onHandleSaveTransform = async (values, id) => {
const { graph } = FlowGraph;
const {
inputSchema = [],
outputSchema = [],
model = [],
outputInitSchema = [],
...others
} = values;
const cell = graph.getCellById(id); // 获取选择节点
const oldData = cell.attr();
const newData = {
...oldData,
nodeData: values,
};
cell.updateAttrs(newData);
message.success('配置成功');
// 不返回不会关闭弹框
changeDrawerType('none');
form.resetFields();
return true;
};
这块逻辑非常容易懂,通过 cell.attr()
可以获取到对应节点的上一状态信息,数据都保存
在我们定义的 attr: { nodeData: ... }
中,合并数据后调用 cell.updateAttrs(...)
就完成了节点的数据更新。
当然只是更新了肯定是不够的,表单数据的获取和回填也很重要。在X6中的节点生表单的数据保存成时都会自动生成一个唯一标识id,这里我们从外层将节点数据 cellData
传入 ,通过 graph.getCellById(id)
方法我们获取到对应的cell元素,cell.isNode()
判断是否为节点,因为需要根据前置节点Source的配置数据做表单逻辑处理,这里用到 graph.getPredecessors()
方法获取到前置所有节点list,因为只有一个前置所以取第一个数据,后面就可以自由写逻辑做数据回填了。具体操作如下,这么看存取操作是不是很简单呢!
import React, { useEffect, useState, useRef, useMemo } from 'react';
...
const cellRef = useRef();
useEffect(() => {
if (cellData?.id) {
const { graph } = InitGraph;
const cell = graph.getCellById(cellData?.id); // 获取选择节点
if (!cell || !cell.isNode()) {
return;
}
cellRef.current = cell;
setCellType(cell.attr('transformType'));
const prevCell = graph.getPredecessors(cellRef.current)[0];
cellData?.nodeData && setOutputDataSource(cellData?.nodeData?.outputSchema || []); // 存在历史数据则不用请求,直接展示历史数据
if (isEmpty(prevCell?.attr('nodeData'))) return;
const prevFormData = prevCell?.attr('nodeData');
setPreSourceData(prevFormData);
getInputTableData({
dataNodeId: prevFormData?.sourceNodeId,
database: prevFormData?.sourceInstance,
table: prevFormData?.dbTable,
});
}
}, [cellData?.id]);
...
数据处理完,最后一步我们需要对数据进行一系列的校验拦截,因为存在节点与节点间的联动逻辑判断,同时也需要配合后端的数据结构,对画布数据的原始结构做一波处理,大致调整为
[
{
source:{...},
tranform:{...},
sink:{...}
},
...
]
代码部分展示如下,这里我主要的几个判断是:
节点数据的非空判断;
是否存在插件节点的前/后置节点未连接情况, 保证画布配置的链接是否符合业务规范;
各节点配置的完整性;
// 处理数据并提交
const filterNodesSubmit = async () => {
const { graph } = FlowGraph;
let isPass = false; // 是否能提交
// 定义一个全局的nodesData数组
const nodesData = [];
// 获取所有节点元素
const cells = graph.getNodes();
// 遍历所有节点元素
try {
cells.forEach(cell => {
const nodeId = cell.id;
if (isEmpty(cell.attr('nodeData'))) {
message.warning('存在节点信息配置为空,请检查链路信息');
throw Error();
}
// 判断节点的id在nodesData中的各类型中的id都不存在的情况
if (
!nodesData.some(
data =>
data.source?.id === nodeId ||
data.transform?.id === nodeId ||
data.sink?.id === nodeId,
)
) {
// 获取前后节点是否存在并获取值
const incomingEdges = graph.getIncomingEdges(cell) || [];
const outgoingEdges = graph.getOutgoingEdges(cell) || [];
// 判断节点是否有前置节点并且类型为source,当前节点为sink,并且前置节点为source
if (cell.attr('action') === 'source' && outgoingEdges.length === 0) {
message.warning('存在数据源配置缺少后续数据目标或数据转化插件,请检查链路信息');
throw Error();
}
if (cell.attr('action') === 'sink' && incomingEdges.length === 0) {
message.warning('配置数据目标前请先配置数据转化插件或数据源信息,并进行连接');
throw Error();
}
if (
cell.attr('action') === 'transForm' &&
incomingEdges.length === 0 &&
outgoingEdges.length === 0
) {
message.warning('存在转化插件链路配置不规范,请检查链路信息');
throw Error();
}
if (
incomingEdges.length > 0 &&
outgoingEdges.length === 0 &&
cell.attr('action') === 'sink' &&
incomingEdges[0]?.getSourceCell()?.attr('action') === 'source'
) {
const sourceNode = incomingEdges[0].getSourceCell();
if (isEmpty(sourceNode.attr('nodeData'))) {
message.warning('存在数据源信息配置不规范,请检查链路信息');
throw Error();
}
if (isEmpty(cell.attr('nodeData'))) {
message.warning('存在数据目标信息配置不规范,请检查链路信息');
throw Error();
}
nodesData.push({
source: { ...sourceNode.attr('nodeData'), id: sourceNode?.id },
sink: { ...cell.attr('nodeData'), id: cell?.id },
});
}
...
// 判断当前节点类型为transform并且有前置和后置节点
if (cell.attr('action') === 'transForm') {
if (incomingEdges.length === 0) {
message.warning('配置数据转化插件前请先配置数据源信息,并进行连接');
throw Error();
}
if (outgoingEdges.length === 0) {
message.warning('存在转化插件缺少数据目标信息');
throw Error();
}
if (isEmpty(cell.attr('nodeData'))) {
message.warning('存在转化插件配置不规范,请检查链路信息');
throw Error();
}
const sourceNode = incomingEdges[0].getSourceCell();
const targetNode = outgoingEdges[0].getTargetCell();
...
nodesData.push({
source: { ...sourceNode.attr('nodeData'), id: sourceNode?.id },
transform: {
options: {
model,
inputSchema: JSON.stringify(inputSchema),
outputSchema: JSON.stringify(outputSchema),
...others,
},
database,
table,
optionType,
},
sink: { ...targetNode.attr('nodeData'), id: targetNode?.id },
});
}
}
});
} catch (err) {
console.log(err);
return;
}
if (isEmpty(nodesData)) {
message.warning('存在链路配置不规范,请检查链路信息');
return;
}
isPass = true;
// 校验通过,返回提交后台的整体数据
...
};
这里使用了 graph.getIncomingEdges()
、graph.getOutgoingEdges()
方法,按官方文档解释
是:分别获取当前节点输入和输出边,直接点就是连接当前节点的上下游边。我们用这个方法
可以判断是否有连接且边的数量是否唯一,因为同一个插件只能允许一个source、sink连接。
然后通过 getSourceCell()
和 getTargetCell()
获取类型为插件节点的上下游边的source、sink节
点在做逻辑判断,最后组装了完整的业务数据给到后端,自此就完成了一整个逻辑校验与数据
提交。
3.3 功能落地
经过一系列关于上述复杂的数据交互和逻辑处理后,最终落地了任务流编排得可视化功能初版,具体实现效果展示如下:
四、 总结与思考
本文介绍的内容比较碎片化,其实X6
是一个功能很强大的库,可以开箱即用、定制化能力也很丰富,这里介绍的也只是一部分能力,如果想学习的还是需要自己去实践下。
在项目实践的过程中,也加深了我对前端可视化场景的思考,怎么根据需求去确定选型方向其实很重要,前期的准备是必不可少的。
我们在选择前端框架时,需要综合考虑项目需求、团队技术栈以及框架的特性和系统适配性,比如上手成本、运行性能、是否支持移动/客户端使用、项目依赖间的兼容性喝后期的维护成本等等。当你明确了这些方向后,其实能减少很多无用功,从而提高开发效率。当然市场上常见的Butterfly、JointJS、G6也有各
自适配的业务场景,选择适合自己的技术
才是最好的选择。
随着前端技术的不断发展,前端可视化方向的技术也在不断演进,未来也许还可以结合大数据、AI等新技术,做出更智能、更高效的方案出来,想想还是挺有意思的事,所以还需要继续学习,持续拥抱新技术。
五、 参考文献
官网:X6官方文档
羽雀:X6问题FAQ
博文:前端流程图框架对比选型
博文:「AntV X6」从 5 个核心要素出发,快速上手AntV X6图可视化编排
博文:工作流引擎设计
原文链接:https://juejin.cn/post/7356892796613115916 作者:Jack_Fang