前言
X6·图编辑引擎
X6 是基于 HTML 和 SVG 的图编辑引擎,提供低成本的定制能力和开箱即用的内置扩展,方便我们快速搭建 DAG 图、ER 图、流程图、血缘图等应用。
需求展示
复杂事件处理 CEP [规则推导]
核心功能清单分析
当你列举的足够细时,你的大任务将被拆成很小的任务,因此每一个TODO项可以预估到0-2h内,每天的效率高
左侧树+产生节点
- 树节点拖拽
- 产生节点拖拽
中间画布
- 左侧业务节点渲染
- 右侧节点渲染
- 节点连线规则
- 容器节点点击
- 数据回显和提交
右侧逻辑节点
- 节点拖拽
总结核心功能引出需要解决问题
- 如何实现拖拽生成节点 [Q1]
- 如何定制生成节点的样式,包括非法样式以及合法样式切换[Q2]
- 怎么样响应业务事件,包括连线,业务校验 [Q3]
- 画布数据搜集和回显 [Q4]
一览X6的能力
快速上手
1.初始化
//目标容器
<div id="container"></div>
// 核心包
import { Graph } from '@antv/x6'
// 画布对象
const graph = new Graph({
container: document.getElementById('container'),
width: 800,
height: 600,
background: {
color: '#F2F7FA',
},
})
- 节点数据
const data = {
nodes: [
{
id: 'node1',
shape: 'custom-react-node',
x: 40,
y: 40,
label: 'hello',
},
{
id: 'node2',
shape: 'custom-react-node',
x: 160,
y: 180,
label: 'world',
},
],
edges: [
{
shape: 'edge',
source: 'node1',
target: 'node2',
label: 'x6',
attrs: {
line: {
stroke: '#8f8f8f',
strokeWidth: 1,
},
},
},
],
}
// 调用渲染
graph.fromJSON(data)
graph.centerContent()
- 插件
// 安装插件包
import { Snapline } from '@antv/x6-plugin-snapline'
// 初始化时使用
graph.use(
new Snapline({
enabled: true,
}),
)
- 画布数据
graph.toJSON()
答案:[Q4]
X6 基础
总结
- 和[Q2]相关的信息包括 ‘节点’、‘连接桩’、‘事件’
- 和[Q3]相关的信息应该包括 ‘交互’
Feel
看完基础,距离我们的需求好像差很多内容,不急进阶一把。
X6 进阶
自定义节点
定义边/连接桩
总结
- 进阶完后,大概一览出节点、边、连接点的大致信息
- 交互动作的相关信息
X6 插件
使用插件
- 下载对应包
- 实例化
- graph.use(Plugin)
答案:[Q1] Dnd 插件
DnD插件的使用
import { Dnd } from '@antv/x6-plugin-dnd';
const graph = new Graph({ background: { color: '#F2F7FA', }, })
const dnd = new Dnd({ target: graph, })
// react-component
export default () => {
const startDrag = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
// 该 node 为拖拽的节点,默认也是放置到画布上的节点,可以自定义任何属性
const node = graph.createNode({ shape: 'rect', width: 100, height: 40, })
dnd.start(node, e.nativeEvent)
}
return ( <ul> <li onMouseDown={startDrag}></li> </ul> )
}
// 总结一下
1. 在 vue 中设置多个dnd 元素引用,绑定 DND-target;
2. 响应dnd实例的start事件生成对应节点,既可以完成节点拖拽响应以及对应位置更新事件
官方demo
实战
构建Vue项目以及安装相关依赖
- npm create vue@latest project-name = [cep-x6]
- cd cep-x6 && npm install
- npm run dev
- 安装依赖
npm i @antv/x6 element-plus
- 修改项目文件如下
- 快速新建视图
- 树组件和拖拽事件注册
- 逻辑组件
定制Graph
- 初始化graph对象
- 可设置选项
- 画布背景
- 宽高
- 支持鼠标滚动缩放
- 自动响应容器resize
- 网格参数
定制插件
定制插件 DnD 以及对齐线 Snapline
-
Snapline
-
Dnd
-
第一步 注册引用
-
第二步 连接拖拽事件
-
第三步 加入原生节点测试
-
定制节点
通过官方的示例一览
markup
markup指定了渲染节点/边时使用的 SVG/HTML 片段,使用 JSON 格式描述
attrs
属性选项
attrs是一个复杂对象,该对象的 Key 是节点 Markup 定义中元素的选择器(selector),对应的值是应用到该 SVG 元素的 SVG 属性值(如 fill 和 stroke),如果你对 SVG 属性还不熟悉,可以参考 MDN 提供的填充和边框入门教程。
拆分节点组成
编码制定节点
- 书写配置
import { Graph } from '@antv/x6'
import { genCustomNode } from './node.helper'
// 逻辑节点容器
export const LOGIC_NODE_CONTAINER = 'Container'
// 树节点
export const TREE_NODE = 'TreeNode'
export interface IConfig {
[LOGIC_NODE_CONTAINER]: string
[TREE_NODE]: string
}
export const VALID_UI = {
fill: '#f99b8d',
stroke: '#f12000'
}
export const INVALID_UI = {
fill: '#fff',
stroke: '#d9dde1'
}
export interface CustomNodeConfig {
componentKey: keyof IConfig
label: string
bussiness?: { [key: string]: any }
width: number
height: number
xlink?: string
}
// 节点配置
export const nodesConfig: { [key in keyof IConfig]: CustomNodeConfig } = {
Container: {
componentKey: LOGIC_NODE_CONTAINER,
label: '容器',
bussiness: {
nodes: []
},
width: 56,
height: 56,
xlink: './xlinks/container.svg'
},
TreeNode: {
componentKey: TREE_NODE,
label: '树节点',
width: 134,
height: 24
}
}
// 注册自定义的节点
export default function registryNode() {
for (const k in nodesConfig) {
Graph.registerNode(k as any, genCustomNode(k as keyof IConfig))
}
}
- 书写生成函数
import type { Markup, Registry } from '@antv/x6'
import { INVALID_UI, nodesConfig, type IConfig } from './node.config'
// body
const genBody = (componentKey: keyof IConfig) => {
const { width, height } = nodesConfig[componentKey]
const config: { markup: Markup; attrs: Registry.Attr.SimpleAttrs } = {
markup: {
tagName: 'rect',
selector: 'body'
},
attrs: {
...INVALID_UI,
rx: 4,
ry: 4,
strokeWidth: 1,
width,
height
}
}
return config
}
// label
const genLabel = (componentKey: keyof IConfig, text: string) => {
const fontSize = 14
const { xlink } = nodesConfig[componentKey]
const textPos = xlink
? {
refX: '50%',
refY: '100%',
refY2: fontSize
}
: {
refY: '50%',
refY2: 1,
textVerticalAnchor: 'middle'
}
const config: { markup: Markup; attrs: Registry.Attr.SimpleAttrs } = {
markup: {
tagName: 'text',
selector: 'label'
},
attrs: {
text: text,
...textPos,
fontSize,
textAnchor: 'middle',
fill: '#001833'
}
}
return config
}
// logo
const genLogo = (componentKey: keyof IConfig, link: string) => {
const { width, height } = nodesConfig[componentKey]
const config: { markup: Markup; attrs: Registry.Attr.SimpleAttrs } = {
markup: {
tagName: 'image',
selector: 'logo'
},
attrs: {
'xlink:href': link,
width,
height,
x: 0,
y: 0
}
}
return config
}
export const genCustomNode = (componentKey: keyof IConfig, metadata?: { [key: string]: any }) => {
const { label, bussiness, width, height, xlink } = nodesConfig[componentKey]
const body = genBody(componentKey)
const nodeLabel = genLabel(componentKey, metadata?.label ?? label)
const node: { [key: string]: any } = {
width,
height,
attrs: {
body: body.attrs,
label: nodeLabel.attrs
},
markup: [body.markup, nodeLabel.markup],
data: { ...(bussiness || {}), ...(metadata || {}) }
}
if (xlink) {
const logo = genLogo(componentKey, xlink)
node.attrs = { ...node.attrs, logo: logo.attrs }
node.markup = [...node.markup, logo.markup]
}
return node
}
-
使用并调用
存一个tag:dev 1.0.1
定制连接桩
定义连接桩分组
import type { Registry, Markup } from '@antv/x6'
import { INVALID_UI } from './node.config'
export interface PortGroupMetadata {
markup?: Markup // 连接桩 DOM 结构定义。
attrs?: Registry.Attr.CellAttrs // 属性和样式。
zIndex?: number | 'auto' // 连接桩的 DOM 层级,值越大层级越高。
// 群组中连接桩的布局。
position?: [number, number] | string | { name: string; args?: object }
label?: {
// 连接桩标签
markup?: Markup
position?: {
// 连接桩标签布局
name: string // 布局名称
args?: object // 布局参数
}
}
}
export interface PortMetadata {
id?: string // 连接桩唯一 ID,默认自动生成。
group?: string // 分组名称,指定分组后将继承分组中的连接桩选项。
args?: object // 为群组中指定的连接桩布局算法提供参数, 我们不能为单个连接桩指定布局算法,但可以为群组中指定的布局算法提供不同的参数。
markup?: Markup // 连接桩的 DOM 结构定义。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。
attrs?: Registry.Attr.CellAttrs // 元素的属性样式。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。
zIndex?: number | 'auto' // 连接桩的 DOM 层级,值越大层级越高。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。
label?: {
// 连接桩的标签。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。
markup?: Markup // 标签 DOM 结构
position?: {
// 标签位置
name: string // 标签位置计算方法的名称
args?: object // 标签位置计算方法的参数
}
}
}
interface PortGroup {
in: string
out: string
}
export default {
in: {
position: 'left',
attrs: {
circle: {
magnet: true,
stroke: INVALID_UI.stroke,
fill: INVALID_UI.fill,
r: 5
}
}
},
out: {
position: 'right',
attrs: {
circle: {
magnet: true,
stroke: INVALID_UI.stroke,
fill: INVALID_UI.fill,
r: 5
}
}
}
} as { [groupName in keyof PortGroup]: PortGroupMetadata }
给节点新增连接桩
-
新增port.config.ts
import type { Registry, Markup } from '@antv/x6' import { INVALID_UI } from './ui.config' export interface PortGroupMetadata { markup?: Markup // 连接桩 DOM 结构定义。 attrs?: Registry.Attr.CellAttrs // 属性和样式。 zIndex?: number | 'auto' // 连接桩的 DOM 层级,值越大层级越高。 // 群组中连接桩的布局。 position?: [number, number] | string | { name: string; args?: object } label?: { // 连接桩标签 markup?: Markup position?: { // 连接桩标签布局 name: string // 布局名称 args?: object // 布局参数 } } } export interface PortMetadata { id?: string // 连接桩唯一 ID,默认自动生成。 group?: string // 分组名称,指定分组后将继承分组中的连接桩选项。 args?: object // 为群组中指定的连接桩布局算法提供参数, 我们不能为单个连接桩指定布局算法,但可以为群组中指定的布局算法提供不同的参数。 markup?: Markup // 连接桩的 DOM 结构定义。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。 attrs?: Registry.Attr.CellAttrs // 元素的属性样式。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。 zIndex?: number | 'auto' // 连接桩的 DOM 层级,值越大层级越高。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。 label?: { // 连接桩的标签。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。 markup?: Markup // 标签 DOM 结构 position?: { // 标签位置 name: string // 标签位置计算方法的名称 args?: object // 标签位置计算方法的参数 } } } export interface PortGroup { in: string out: string } export type Subsequence<T extends any[], Prefix extends any[] = []> = T extends [ infer F, ...infer R ] ? Subsequence<R, Prefix | [...Prefix, F]> : Prefix export default { in: { position: 'left', attrs: { circle: { magnet: true, stroke: INVALID_UI.stroke, fill: INVALID_UI.fill, r: 5 } } }, out: { position: 'right', attrs: { circle: { magnet: true, stroke: INVALID_UI.stroke, fill: INVALID_UI.fill, r: 5 } } } } as { [groupName in keyof PortGroup]: PortGroupMetadata } -
修改节点数据配置和生成函数
-
效果
多加几个节点
// HomeView.vue
const handleMetadataDrag = (info: { e: DragEvent; data: any }) => {
;(info.e as any).dropEffect = 'move'
const json = genCustomNode(info.data.componentKeys)
const node = graph.createNode({
id: String(Date.now()),
shape: info.data.componentKeys,
...json
})
elDnD.start(node, info.e)
}
至此: 已经离我们的需求越来越近了 dev-tag 1.0.2
定制边
// edge.config.ts
import { Edge, Graph } from '@antv/x6'
export const CUSTOM_EDGE = 'custom-edge'
Edge.config({
markup: [
{
tagName: 'path',
selector: 'line',
attrs: {
fill: 'none'
}
},
{
tagName: 'path',
selector: 'outline',
attrs: {
fill: 'none'
}
}
],
connector: { name: 'smooth' },
attrs: {
line: {
connection: true,
stroke: '#8d98a4',
strokeDasharray: 3,
strokeWidth: 1,
targetMarker: {
name: 'block',
size: 8,
fill: '#fff'
}
},
outlin: {
connection: true,
stroke: 'transparent',
strokeWidth: 5
}
}
})
export default function registryEdge() {
Graph.registerEdge(CUSTOM_EDGE, Edge)
}
可喜可贺:我们完成节点,连接桩,连线的自定义了,里程碑!【dev-tag 1.0.3】
设计业务数据与交互校验
接下来我们简单分析实现下业务数据的携带以及更多的交互设计
-
数据节点的弹窗交互【注册画布事件】
-
连线校验
// 连线高亮
highlighting: {
magnetAdsorbed: {
name: 'stroke'
},
magnetAvailable: {
name: 'stroke'
}
},
connecting: {
snap: { radius: 30 }, // 自动吸附范围
allowBlank: false, // 允许连接空白
allowLoop: false, // 允许连自己
allowNode: false, // 直接连接节点
highlight: true, // 开启高亮
validateMagnet({ magnet }) {
// 不允许 in 作为起点
return magnet.getAttribute('port-group') !== 'in'
},
validateConnection({ sourceMagnet, targetMagnet }) {
if (!sourceMagnet || sourceMagnet.getAttribute('port-group') === 'in') {
return false
}
if (!targetMagnet || targetMagnet.getAttribute('port-group') !== 'in') {
return false
}
return true
},
createEdge() {
return graph.createEdge({ shape: CUSTOM_EDGE })
}
}
- 业务数据
- 在响应节点事件时,从节点取出data cell.data [getData]
- 重新注入data, setData,坑点 注意对象结构的data,需要制定option的merge级别
画布数据渲染与收集
- 提交阶段,收集所有节点的数据 graph.toJSON,处理节点与节点之间的 in & out 关系,形成一个链路
const payload = {
draft: { // 画布数据
version: '1.0',
cells: [...], // 所有节点数据,包括node,edge
},
trigger: [
{id: 'xxx0', in: [], out: ['xxx1'], props: {}},
{id: 'xxx1', in: ['xxx0'], out: ['xxx2'], props: {}},
{id: 'xxx2', in: ['xxx1'], out: [], props: {}},
] // 业务数据
}
-
回显阶段,在画布加载前拉取到应用数据,使用其中cells数组,渲染graph后调用 graph.fromJSON(cells)
-
节点交互设置,graph.on('node:mouseenter') + graph.on('node:mouseleave')
-
连线状态,节点的业务数据中携带 uiState [Boolean],初始化时都是 false,校验时机:连线+弹窗属性设置
应用方向
- CEP
- 策略
- 机柜列+机柜+刀箱
- 关系拓扑
- 流程图
- 业务流转示意图等
总结
- 完全掌握并快速开发需求,需要时间去研究
- 好事多磨