通过对BPMN的深入学习,以及对业界成熟的流程编排设计器的调研,了解到要研发一个流程编排设计器,需要实现如下几个主要的功能:
除了设计器本身的能力,还需要物料区域,工具栏操作区域,属性设置区域等额外的功能。
基于阿里低代码引擎(Low-Code Engine)强大的定制扩展能力,自定义开发了缩放、组件面板、AntV X6画布面板、设置器面板等功能,构建出遵循BPMN规范的流程编排设计器。
Addon
拖拽能力完成物料到画布的编排功能;以下内容将重点介绍一下基于AntV X6
实现流程编排画布的过程,以及介绍在开发过程中重点用到的配置项和使用方法。当然设计器的内容除了画布,还有物料区、设置区、协议转换等内容,在这里不做过多阐述。
在页面中创建一个画布容器x6-container
,用于绘制BPMN流程图。期间需要构造画布配置参数GraphOptions
,初始化画布对象。
import React from "react";
export default props => {
const containerRef = useRef(null);
useLayoutEffect(() => {
// 初始化图形
registerShape();
// 初始化画布
let options = getDefaultGraphOptions(containerRef.current);
const _graph = new Graph(options);
// 初始化撤销重做、快捷键、图形变换等插件
initPlugins(_graph);
// 初始化删除,双击等事件
initEvents(_graph);
}, []);
return (
);
};
通过配置 connecting 可以实现丰富的连线交互。
采用了曼哈顿算法,注意需要使用excludeShapes
排除对于Group
节点的计算
{
router: {
name: "manhattan",
args: {
excludeShapes: [ElementType.Group],
padding: 25
}
},
}
自定义新建边的样式,在动态拖拽生成线条时使用。
{
createEdge() {
return new Shape.Edge({
shape: ElementType.SequenceFlow,
router: {
name: "manhattan",
args: {
excludeShapes: [ElementType.Group],
padding: 25
}
},
});
},
}
通过 embedding 可以将一个节点拖动到另一个节点中,使其成为另一节点的子节点,默认禁用。本项目中用于支持向Group节点中拖动子节点达到分组展示的效果。
{
embedding: {
enabled: true, // 是否允许节点之间嵌套
findParent({ node }) {
const bbox = node.getBBox();
return this.getNodes().filter(item => {
const data = item.getData();
if (data && data.parent) {
item.toBack(); // 修改Group节点zIndex,解决拖拽覆盖问题
const targetBBox = item.getBBox();
return bbox.isIntersectWithRect(targetBBox);
}
return false;
});
},
},
}
限制节点和边的交互行为,实际案例中用到了edgeLabelMovable
,支持边的标签可移动。
nodeMovable
节点是否可以被移动。edgeMovable
边是否可以被移动。edgeLabelMovable
边的标签是否可以被移动。在BPMN
规范中,存在事件、网关、活动等基础元素,因此定义了如下几种元素类型,并分别注册了节点和边。每种图形都有自身的属性配置。
export enum ElementType {
/** 开始事件 */
StartEvent = "startEvent",
/** 结束事件 */
EndEvent = "endEvent",
/** 错误结束事件 */
ErrorEndEvent = "errorEndEvent",
/** 连接线 */
SequenceFlow = "sequenceFlow",
/** 排他网关 */
ExclusiveGateway = "exclusiveGateway",
/** 并行网关 */
ParallelGateway = "parallelGateway",
/** 功能节点 */
FunctionTask = "functionTask",
/** 分组 */
Group = "group",
}
配置节点各类属性信息,并注册节点。这里以Group
元素为例,代码如下:
// 分组元素配置信息
const defaultGroupConfig = {
inherit: "rect",
width: 240,
height: 160,
zIndex: 0,
markup: [
{
tagName: 'rect',
selector: 'body',
},
{
tagName: 'text',
selector: 'text',
},
],
attrs: {
text: {
refX: 0.5,
refY: 10,
textAnchor: 'middle',
textVerticalAnchor: 'top',
// 文本换行:https://antv-x6.gitee.io/zh/docs/api/registry/attr/#textwrap
textWrap: {
width: 200,
height: 30,
ellipsis: true,
},
},
body: {
rx: 6,
ry: 6,
strokeWidth: 1,
strokeDasharray: "3,3",
stroke: "#bfbfbf",
fill: "rgba(238,238,238,.3)",
},
},
data: {
parent: true,
},
}
Graph.registerNode(EElementType.Group, defaultGroupConfig, true);
markup
指定了渲染节点时使用的 SVG片段,使用 JSON 格式描述。如上代码则表示节点内部包含
和
两个SVG元素,渲染到页面之后,节点对应的元素如下:
分组
指定需要创建哪种 SVG/HTML 元素
该元素的唯一选择器,通过选择器为该元素指定属性样式
属性选项 attrs 是一个复杂对象,该对象的 Key 是节点 Markup 定义中元素的选择器(selector),对应的值是应用到该 SVG 元素的 SVG 属性值(如 fill 和 stroke),如果你对 SVG 属性还不熟悉,可以参考 MDN 提供的填充和边框入门教程。
Graph.registerEdge(
ElementType.SequenceFlow,
{
inherit: "edge",
attrs: {
line: {
strokeWidth: Size.LineStrokeWith,
stroke: Colors.Line
}
},
router: {
name: "manhattan",
args: {
excludeShapes: [ElementType.Group], // 解决Group节点下manhattan算法失效问题,导致线条显示异常
padding: 25
}
}
},
true,
);
使用@antv/x6-plugin-keyboard
,为画布绑定快捷键,例如复制,粘贴,删除等
import { Graph } from "@antv/x6";
import { Keyboard } from "@antv/x6-plugin-keyboard";
export const initPlugins = (graph: Graph) => {
graph.use(
new Keyboard({
enabled: true,
})
);
};
使用@antv/x6-plugin-snapline
,在移动节点时辅助排版
import { Graph } from "@antv/x6";
import { Snapline } from "@antv/x6-plugin-snapline";
export const initPlugins = (graph: Graph) => {
graph.use(
new Snapline({
enabled: true,
}),
);
使用@antv/x6-plugin-history
,实现元素操作的撤销和重做
import { Graph } from "@antv/x6";
import { History } from "@antv/x6-plugin-history";
export const initPlugins = (graph: Graph) => {
graph.use(
new History({
enabled: true,
beforeAddCommand(event, args: any) {
// console.log(event, args);
if (args.key === "tools") {
return false;
}
return true;
},
}),
);
使用@antv/x6-plugin-selection
,可以实现点击元素选中,启用多选能力,按住Ctrl/Command
后点击元素可以多选
import { Graph } from "@antv/x6";
import { Selection } from "@antv/x6-plugin-selection";
export const initPlugins = (graph: Graph) => {
graph.use(
// 支持节点选中样式
new Selection({
enabled: true,
}),
);
};
使用@antv/x6-plugin-transform
,实现节点大小的调整,节点渲染角度的调整,例如本项目中的Group
节点就需要进行大小调整。
import { Graph } from "@antv/x6";
import { Transform } from "@antv/x6-plugin-transform";
export const initPlugins = (graph: Graph) => {
graph.use(
new Transform({
resizing: {
enabled: node => {
// 支持分组节点调整大小
if (node.shape === "group") {
return true;
}
return false;
},
},
}),
);
};
使用@antv/x6-plugin-dnd
,通过拖拽交互往画布中添加节点,本项目中需要从流程图组件库中拖拽组件到画布中。
import { Dnd } from "@antv/x6-plugin-dnd";
const ComponentPanel = props => {
const dndRef = useRef();
const dndContainerRef = useRef(null);
useLayoutEffect(() => {
const _dnd = new Dnd({
target: graph, //来源全局Graph实例,此处省略
scaled: false,
dndContainer: dndContainerRef.current as any,
});
dndRef.current = _dnd;
}
const startDrag = useCallback((e: React.MouseEvent, data: any) => {
const target = e.currentTarget;
const type = target.getAttribute("data-type");
const nodeConfig: any = {
shape: type,
label: data.title,
data: {
...data
}
};
// 此处省略Graph实例获取
const node = graph.createNode(nodeConfig);
dndRef.current.start(node, e.nativeEvent as any);
}, []);
return (
);
}
监听边的选中和取消选中事件,对应修改线条样式内容。
export const initEvents = (graph: Graph) => {
graph.on("edge:selected", ({ edge }) => {
edge.toFront();
edge.attr({
line: {
stroke: Colors.LineActived,
strokeWidth: 2
},
})
});
graph.on("edge:unselected", ({ edge }) => {
edge.attr({
line: {
stroke: Colors.Line,
strokeWidth: Size.LineStrokeWith
},
})
});
}
监听节点的鼠标移入、移出事件,控制链接桩的显示和隐藏
export const initEvents = (graph: Graph) => {
const showPorts = (ports: NodeListOf, show: boolean) => {
for (let i = 0, len = ports.length; i < len; i += 1) {
ports[i].style.visibility = show ? 'visible' : 'hidden'
}
}
graph.on('node:mouseenter', () => {
const container = document.getElementById('graph-container')!
const ports = container.querySelectorAll(
'.x6-port-body',
) as NodeListOf
showPorts(ports, true)
})
graph.on('node:mouseleave', () => {
const container = document.getElementById('graph-container')!
const ports = container.querySelectorAll(
'.x6-port-body',
) as NodeListOf
showPorts(ports, false)
})
}
监听节点的双击事件,添加小工具编辑节点名称
export const initEvents = (graph: Graph) => {
graph.on('node:dblclick', ({ cell, e }) => {
const name = 'node-editor';
cell.removeTool(name)
cell.addTools({
name,
args: {
event: e,
attrs: {
backgroundColor: '#fff',
},
},
})
})
}
使用低代码引擎提供的插件API,移除低代码引擎默认的画布,添加自定义开发的画布组件(AntV X6),方式如下:
const PluginX6Designer = (ctx: ILowCodePluginContext) => {
return {
init() {
const { skeleton, project } = ctx;
skeleton.remove({
name: 'designer',
area: 'mainArea',
type: 'Widget'
});
skeleton.add({
area: 'mainArea',
name: 'designer',
type: 'Widget',
content: X6Designer,
contentProps: {
ctx,
}
});
}
}
}
PluginX6Designer.pluginName = 'plugin-x6-designer';
export default PluginX6Designer;
// 注册X6画布
await plugins.register(PluginX6Designer);
涉及业务敏感信息已做模糊处理
基于AntV X6
实现流程编排设计器,有如下几个方面的优点:
开发过程还是漫长和曲折的,需要不断熟悉查阅官网API,做各种类型和效果的尝试。需要说明的是,在本项目开发时,低代码引擎和X6相结合还没有开源的解决方案,因此在熟悉了两者的文档后,终于探索出一条结合的道路。因此在实现拖拽的交互逻辑中则是采用了AntV X6
的DND
插件。同样也可以采用低代码引擎本身的拖拽机制,有兴趣可以官网了解开源的方案。