通过对BPMN的深入学习,以及对业界成熟的流程编排设计器的调研,了解到要研发一个流程编排设计器,需要实现如下几个主要的功能:
- 支持创建各种流程图元素,包括任务(活动)、网关、事件等。
- 支持绘制各种连接线,包括普通连接线、条件连接线、消息连接线等。
- 支持流程图的编辑,包括元素的选择、移动、删除、复制、粘贴等。
- 支持流程图的保存,包括保存流程图数据、导入/导出流程图数据等。
- 具备良好的可扩展性,可以通过插件等方式进行功能扩展和定制化。
- 具备良好的用户体验,包括界面美观、交互友好等。
除了设计器本身的能力,还需要物料区域,工具栏操作区域,属性设置区域等额外的功能。
技术方案
基于阿里低代码引擎(Low-Code Engine)强大的定制扩展能力,自定义开发了缩放、组件面板、AntV X6画布面板、设置器面板等功能,构建出遵循BPMN规范的流程编排设计器。
- 组件面板基于标准的物料协议完成物料元素渲染;
- 画布面板基于 AntV X6 实现,借助自身
Addon拖拽能力完成物料到画布的编排功能; - 设置器面板基于 Formily 表单协议完成各类配置表单渲染;
- 协议转换基于fast-xml-parser,完成Graph JSON 和 XML 之间的协议转换。
实现步骤
以下内容将重点介绍一下基于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 ( <div className="lc-designer lowcode-plugin-designer"> <div className="lc-project"> <div className="lc-simulator-canvas lc-simulator-device-default"> <div id="x6-container" ref={containerRef} /> </div> </div> </div> ); };
讯享网
连线(connecting)
通过配置 connecting 可以实现丰富的连线交互。
router
采用了曼哈顿算法,注意需要使用excludeShapes排除对于Group节点的计算
讯享网{ router: { name: "manhattan", args: { excludeShapes: [ElementType.Group], padding: 25 } }, }
createEdge
自定义新建边的样式,在动态拖拽生成线条时使用。
{ createEdge() { return new Shape.Edge({ shape: ElementType.SequenceFlow, router: { name: "manhattan", args: { excludeShapes: [ElementType.Group], padding: 25 } }, }); }, }
组合(embedding)
通过 embedding 可以将一个节点拖动到另一个节点中,使其成为另一节点的子节点,默认禁用。本项目中用于支持向Group节点中拖动子节点达到分组展示的效果。
讯享网{ embedding: { enabled: true, // 是否允许节点之间嵌套 findParent({ node }) { const bbox = node.getBBox(); return this.getNodes().filter(item => { const data = item.getData<any>(); if (data && data.parent) { item.toBack(); // 修改Group节点zIndex,解决拖拽覆盖问题 const targetBBox = item.getBBox(); return bbox.isIntersectWithRect(targetBBox); } return false; }); }, }, }
限制(interacting)
限制节点和边的交互行为,实际案例中用到了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
markup 指定了渲染节点时使用的 SVG片段,使用 JSON 格式描述。如上代码则表示节点内部包含<rect>和<text>两个SVG元素,渲染到页面之后,节点对应的元素如下:
<g data-cell-id="d873ae84-1655-4973-9ba8-bd65b6613b80" data-shape="group" class="x6-cell x6-node" transform="translate(-590,20)"> <rect fill="rgba(238,238,238,.3)" stroke="#bfbfbf" stroke-width="1" rx="6" ry="6" stroke-dasharray="3,3" width="240" height="160"></rect> <text font-size="14" xml:space="preserve" fill="#000000" text-anchor="middle" font-family="Arial, helvetica, sans-serif" text="分组" transform="matrix(1,0,0,1,120,10)"> <tspan dy="0.8em" class="v-line">分组</tspan> </text> </g>
tagName
指定需要创建哪种 SVG/HTML 元素
selector
该元素的唯一选择器,通过选择器为该元素指定属性样式

attrs
属性选项 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<any>(); 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<HTMLDivElement, 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 ( <div className="x6-component-panel" ref={dndContainerRef}> <CustomComponent startDrag={startDrag} /> </div> ); }
初始化事件
选中/取消选中
监听边的选中和取消选中事件,对应修改线条样式内容。
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<SVGElement>, 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<SVGElement> showPorts(ports, true) }) graph.on('node:mouseleave', () => { const container = document.getElementById('graph-container')! const ports = container.querySelectorAll( '.x6-port-body', ) as NodeListOf<SVGElement> 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实现流程编排设计器,有如下几个方面的优点:
- 低成本定制能力:AntV X6是基于HTML和SVG的图表编辑引擎,提供了丰富的API和自定义事件,可以通过编写JavaScript代码,快速实现自定义的图表样式、布局和交互方式,满足具体业务需求。
- 高度可扩展性:AntV X6提供了众多内置扩展,包括DAG图、ER图、流程图等应用,可以帮助你根据自己的需要,自由扩展和定制功能和组件,实现更加灵活的业务需求。
- 强大的交互功能:AntV X6提供了丰富的图表组件和交互功能,支持拖拽、连线、缩放等多种交互方式,还可以集成自定义的图表组件和逻辑处理,实现更加丰富的功能需求。
开发过程还是漫长和曲折的,需要不断熟悉查阅官网API,做各种类型和效果的尝试。需要说明的是,在本项目开发时,低代码引擎和X6相结合还没有开源的解决方案,因此在熟悉了两者的文档后,终于探索出一条结合的道路。因此在实现拖拽的交互逻辑中则是采用了AntV X6的DND插件。同样也可以采用低代码引擎本身的拖拽机制,有兴趣可以官网了解开源的方案。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/128125.html