一篇文章带你上手AntV X6

7 阅读12分钟

X6 是 AntV 旗下的图编辑引擎,提供了一系列开箱即用的交互组件和简单易用的节点定制能力,方便我们快速搭建流程图、DAG 图、ER 图等图应用。

X6 图表的构成要素

graph TD
    容器Graph --> 管理承载
    管理承载 --> 节点Node
    管理承载 --> 边Edge

    节点Node --> 基础属性[x,y,width,height,angle]
    节点Node --> 样式定制[attrs,markup,选择器]
    节点Node --> 连接桩[Ports]

    边Edge --> 连接目标[source,target]
    边Edge --> 路径控制[vertices,router,connector]
    边Edge --> 装饰[labels,marker]

    增强插件 --> 常用插件[Snapline,History,Selection]
    增强插件 --> 节点工具[Tools,删除按钮,编辑按钮]
    增强插件 --> 增强交互
    增强交互 --> 容器Graph
    增强交互 --> 节点Node
    增强交互 --> 边Edge

画布Graph

AntV X6 的画布(Graph)是构建图编辑应用的起点和核心容器。它负责管理、渲染画布上的所有元素(如节点和边),并提供了丰富的配置选项和 API 来控制画布本身的行为与外观

创建与基础配置

首先需要在页面中准备一个容器元素(如 div),然后通过实例化 Graph 类来初始化画布

<div id="container"></div>

配置挂载点,背景,画布尺寸,网格

import { Graph } from '@antv/x6';

const graph = new Graph({
  // 挂载点:必须指定
  container: document.getElementById('container'),
  // 画布尺寸:可以固定,也可以不设置以自适应容器
  width: 800,
  height: 600,
  // 背景配置
  background: {
    color: '#F2F7FA', // 设置背景颜色
    // 也可以配置为图片
  },
  // 网格配置
  grid: {
    visible: true, // 显示网格
    size: 10,      // 网格大小,也是移动的最小单位
    type: 'doubleMesh', // 网格类型:dot, fixedDot, mesh, doubleMesh
    args: [
      { color: '#eee', thickness: 1 }, // 主网格线
      { color: '#ddd', thickness: 1, factor: 4 } // 次网格线
    ],
  },
});
画布尺寸控制方式
  • 固定尺寸:在 Graph 配置中显式设置 width 和 height 属性。
  • 容器尺寸:不设置 width 和 height,画布将自动撑满 container 元素的大小。
  • 自适应容器:设置 autoResize: true。当容器大小发生变化时(例如浏览器窗口改变或侧边栏折叠),画布会自动调整大小。注意:使用此功能时,建议在 container 外层套一个宽高为 100% 的父容器以便监听尺寸变化。

交互与行为配置

画布平移

允许用户通过鼠标拖拽来移动视口。

const graph = new Graph({
  panning: {
    enabled: true,
    eventTypes: ['leftMouseDown', 'rightMouseDown', 'mouseWheel'], // 触发方式
  },
});
滚轮缩放

允许用户通过滚动滚轮来缩放画布

const graph = new Graph({
  mousewheel: {
    enabled: true,
    zoomAtMousePosition: true, // 是否以鼠标位置为中心进行缩放
    factor: 1.1,               // 缩放因子
  },
});
节点对齐

通过插件实现,在拖动节点时显示对齐线,辅助排版

import { Snapline } from '@antv/x6';

graph.use(new Snapline({ enabled: true }));

数据渲染与导出

渲染数据

使用 graph.fromJSON(data),其中 data 对象包含 nodes 和 edges 数组。

const data = {
  nodes: [ /* 节点定义 */ ],
  edges: [ /* 边定义 */ ],
};
graph.fromJSON(data);
graph.centerContent(); // 将画布内容居中显示
导出数据

使用 graph.toJSON() 可以将当前画布上的所有元素序列化为 JSON 对象,便于存储或传输。

常用API

  • graph.resize(width, height) :手动调整画布尺寸。
  • graph.translate(tx, ty) :平移画布视口。
  • graph.zoom(factor)  / graph.zoomTo(level) :缩放画布。
  • graph.zoomToFit(options?) :自动缩放并平移,使所有图元适配视口大小。
  • graph.centerContent() :将画布内容的中心点对齐到视口的中心。
  • graph.addNode(nodeMetadata)  / graph.addEdge(edgeMetadata) :动态添加节点或边。

图形元素

基类Cell

抽象基类,定义了所有图元素(节点和边)共有的属性和行为。可以把它想象成图元素的“基因”,无论最终是节点还是边,都继承了这些核心能力。

核心作用
  • 统一数据模型:无论是节点还是边,在内部都被视为 Cell 的实例,方便画布(Graph)进行统一管理和操作。
  • 定义通用接口:提供通用方法
关键属性
属性描述作用
id节点的唯一标识推荐使用有业务意义的 ID,便于数据追踪。
markupSVG/HTML 结构的 JSON 描述定义了元素由哪些基本图形(如 <rect>, <circle>, <text>)组成。这是自定义形状的基础。
attrs样式属性对象通过 CSS 选择器或预定义的“选择器(selector)”来精细化控制 markup 中每个部分的样式(如颜色、边框、文字内容)。
visible是否可见控制元素的显示与隐藏。
data自定义业务数据用于存储与该元素相关的任何业务数据,而不影响渲染逻辑。
zIndex层级控制节点在画布上的层叠顺序。

节点Node

“有形状、有位置、可以附着连接点”的视觉实体。它不仅仅是画布上的一个方块,更是一个高度可配置的数据模型。

核心属性
基础属性
interface NodeMetadata {
  id: string;           // 唯一标识
  markup?: Markup;      // SVG/HTML 结构定义
  attrs?: Attrs;        // 样式属性(支持 CSS 选择器)
  data?: any;           // 自定义业务数据
  visible?: boolean;    // 可见性
  zIndex?: number;      // 层级
}
位置属性(特有)

position: { x: number, y: number }

中心点对齐:默认情况下,节点的 position 是指节点的左上角。如果希望基于中心点定位,需要手动计算:

```ts
// 让节点的中心点在 (200, 150)
node.position(
  node.getPosition().x - node.getSize().width / 2,
  node.getPosition().y - node.getSize().height / 2
);
```
尺寸属性(特有)

size: { width: number, height: number } 定义节点的最小包围盒。 对于特殊形状(如圆形),width === height 通常表示直径。

```js
// 矩形
node.size(100, 50);

// 圆形
node.size(60, 60);  // 直径 60px
```
连接桩

ports 是 Node 上专门用于连接边的锚点集合。通过 Ports,你可以精确控制边的连接位置(如“右侧中间”、“左上角”等)。

Port 的结构
interface PortMetadata {
  id?: string;           // 连接桩唯一标识
  group?: string;        // 所属组(用于批量设置样式)
  args?: Partial<PortArgs>; // 连接点计算参数
  markup?: Markup;       // 连接桩的独立 SVG 结构
  attrs?: Attrs;         // 连接桩的独立样式
  position?: Position;   // 定位策略(内置或自定义)
}
定位策略(position
ports: {
  groups: {
    // 定义一组连接桩
    'top': {
      position: 'top',          // 顶部均匀分布
      attrs: { circle: { fill: '#87e8de' } }
    },
    'bottom': {
      position: 'bottom',       // 底部均匀分布
      attrs: { circle: { fill: '#ffa39e' } }
    }
  },
  items: [
    { id: 'port1', group: 'top' },    // 顶部连接桩
    { id: 'port2', group: 'bottom' },  // 底部连接桩
    { 
      id: 'port3', 
      position: { name: 'absolute', args: { x: 10, y: 20 } } // 绝对定位
    }
  ]
}
核心方法与操作
位置与变换
// 获取位置
const pos = node.getPosition();      // { x, y }
const bbox = node.getBBox();         // 边界框 { x, y, width, height }

// 设置位置
node.setPosition(150, 200);
node.translate(10, -5);              // 相对移动

// 旋转
node.rotate(45);                     // 旋转 45 度
const angle = node.getAngle();
样式更新
// 直接修改 attrs
node.attr('body/fill', '#ff4d4f');    // X6 路径语法
node.attr('title/text', '新标题');

// 批量修改
node.attr({
  body: { stroke: '#faad14' },
  status: { fill: '#52c41a' }
});
连接桩操作
// 添加连接桩
node.addPort({
  id: 'new-port',
  group: 'bottom',
  attrs: { circle: { r: 6 } }
});

// 获取连接桩
const ports = node.getPorts();
const port = node.getPort('port-id');

// 删除连接桩
node.removePort('port-id');

高级特性
组合节点(Node Composition)

X6 支持将多个节点组合成一个逻辑整体:

const group = graph.addNode({
  shape: 'rect',
  x: 100,
  y: 100,
  width: 300,
  height: 200,
  isGroup: true,        // 标记为组合节点
});

// 将子节点放入组合
const child = graph.addNode({
  shape: 'rect',
  x: 150,
  y: 150,
  width: 80,
  height: 40,
  parent: group,        // 指定父节点
});
节点动画

通过 transition 让节点动起来:

node.transition('position', { x: 300, y: 300 }, {
  delay: 1000,
  duration: 2000,
  timing: 'ease-in-out',
});

node.transition('attrs/body/fill', '#ff4d4f', {
  duration: 500,
});
工具(Tools)

为节点添加交互工具:

// 添加删除按钮
node.addTools({
  name: 'button-remove',
  args: { x: '100%', y: 0, offset: { x: -20, y: 20 } }
});

// 添加边界编辑器
node.addTools('boundary');

边Edge

核心属性
连接属性

source 和 target:定义边的两端

// 最简单的形式:直接连接节点
{
  source: 'node1',  // 节点的 ID
  target: 'node2'
}

// 指定连接桩(Port)
{
  source: { cell: 'node1', port: 'out-port1' },  // 从 node1 的 out-port1 出发
  target: { cell: 'node2', port: 'in-port2' }    // 连接到 node2 的 in-port2
}

// 指定连接点(坐标)
{
  source: { x: 100, y: 100 },  // 从画布上的点出发
  target: { cell: 'node2' }     // 连接到节点
}

// 指定连接位置(百分比)
{
  source: { 
    cell: 'node1',
    anchor: { name: 'center' }  // 锚点策略:中心点
  },
  target: {
    cell: 'node2',
    anchor: { name: 'right' }    // 锚点策略:右侧中间
  }
}
路径控制属性

vertices: 中间路径点,定义边必须经过的中间点,实现绕行。

{
  source: 'node1',
  target: 'node2',
  vertices: [
    { x: 200, y: 150 },  // 经过 (200,150)
    { x: 300, y: 200 }   // 然后经过 (300,200)
  ]
}

router: 路由策略, 自动计算路径,避开节点或沿特定方向。

{
  router: {
    name: 'manhattan',  // 曼哈顿路由(避开障碍物,只走水平/垂直线)
    args: {
      padding: 20,       // 与节点的间距
      step: 10           // 网格大小
    }
  }
}

// 其他内置 router
// - 'orth'      : 正交路由(水平/垂直,不一定避开障碍)
// - 'metro'     : 地铁线路风格
// - 'normal'    : 普通直线
// - 'oneside'   : 从节点一侧引出

connector: 连接器,定义路径点之间的连线方式。

{
  connector: {
    name: 'smooth',  // 平滑曲线
    args: {
      direction: 'H'  // 水平方向优先
    }
  }
}

// 其他内置 connector
// - 'straight' : 直线连接(默认)
// - 'rounded'  : 圆角折线
// - 'jumpover' : 跨线(用于电路图)
装饰属性
箭头(Arrowhead)
{
  attrs: {
    line: {
      stroke: '#1890ff',
      strokeWidth: 2,
      targetMarker: {  // 终点箭头
        name: 'block',  // 箭头类型:block, classic, diamond, circle 等
        size: 8,
        fill: '#1890ff'
      },
      sourceMarker: {   // 起点箭头
        name: 'circle',
        r: 4
      }
    }
  }
}
标签(Label)

边上的文字标签:

{
  labels: [
    {
      attrs: {
        text: { text: '关系名称' },
        label: { fill: '#333' }
      },
      position: 0.5  // 位于边的 50% 位置
    }
  ]
}

// 或者通过 markup/attrs 方式
{
  attrs: {
    label: {
      text: '关系名称',
      fill: '#333',
      fontSize: 12,
      refX: 0.5,      // 相对于边的位置
      refY: -10        // 偏移量
    }
  }
}

核心方法与操作
连接操作
// 获取连接的节点
const sourceNode = edge.getSourceNode();
const targetNode = edge.getTargetNode();

// 修改连接
edge.setSource('node3');
edge.setTarget({ cell: 'node4', port: 'port2' });

// 获取连接点坐标
const sourcePoint = edge.getSourcePoint();   // { x, y }
const targetPoint = edge.getTargetPoint();
路径操作
// 获取/设置路径点
const vertices = edge.getVertices();
edge.setVertices([
  { x: 150, y: 150 },
  { x: 250, y: 150 }
]);

// 获取完整路径(所有点,包括起点终点)
const points = edge.getPoints();

// 获取路径的 SVG 字符串
const pathData = edge.getPathData();
标签操作
// 添加标签
edge.appendLabel({
  attrs: {
    text: { text: '新标签' },
    label: { fill: 'red' }
  },
  position: 0.3  // 30% 位置
});

// 获取/设置标签
const labels = edge.getLabels();
edge.setLabelAt(0, { text: '修改标签' });

// 删除标签
edge.removeLabelAt(1);
样式更新
// 修改边样式
edge.attr('line/stroke', '#ff4d4f');
edge.attr('line/strokeDash', [5, 3]);  // 虚线

// 修改箭头
edge.attr('line/targetMarker', {
  name: 'diamond',
  size: 10,
  fill: '#faad14'
});

高级特性
边动画

让边“动起来”,表达数据流或处理过程:

// 添加流动的蚂蚁线
edge.attr('line/strokeDash', [10, 10]);
edge.attr('line/style', {
  animation: 'ant-line 30s infinite linear'
});

// 使用 X6 内置动画
edge.animate('attrs/line/stroke', '#52c41a', {
  delay: 1000,
  duration: 500,
  direction: 'alternate',
  iterations: 3
});

// 移动的小圆点
const circle = edge.getView().find('animateDot');
circle.animate(
  { startOffset: '100%' },
  { duration: 3000, repeat: true }
);
边与交互
// 边点击事件
edge.on('click', () => {
  console.log('边被点击');
  edge.attr('line/stroke', '#ff4d4f');
});

// 高亮经过的边
edge.on('mouseenter', () => {
  edge.attr('line/strokeWidth', 4);
});
edge.on('mouseleave', () => {
  edge.attr('line/strokeWidth', 2);
});

// 添加工具按钮
edge.addTools({
  name: 'button',
  args: {
    markup: [
      {
        tagName: 'circle',
        selector: 'button',
        attrs: {
          r: 12,
          fill: '#fff',
          stroke: '#1890ff',
          cursor: 'pointer'
        }
      },
      {
        tagName: 'text',
        selector: 'icon',
        attrs: {
          text: '×',
          fill: '#1890ff',
          fontSize: 14,
          textAnchor: 'middle',
          textVerticalAnchor: 'middle',
          cursor: 'pointer'
        }
      }
    ],
    onClick({ edge }) {
      edge.remove();  // 点击删除边
    }
  }
});
// 定义状态样式
edge.setStates({
  highlight: { attrs: { line: { strokeWidth: 4 } } },
  error: { attrs: { line: { stroke: '#ff4d4f' } } }
});
```

vue3+AntV X6简易实现可拖拽流程图

1. 模板部分 (Template)

<template>
  <div class="x6-container">
    <!-- 左侧节点库:存放可拖拽的节点模板 -->
    <div class="node-palette">
      <h3>节点库</h3>
      <div 
        v-for="node in nodeTypes" 
        :key="node.type"
        class="palette-node"
        draggable="true"
        @dragstart="handleDragStart($event, node)"
      >
        {{ node.label }}
      </div>
    </div>
    
    <!-- 右侧画布区域:包含画布和工具栏 -->
    <div class="canvas-wrapper">
      <!-- 画布容器:X6实际渲染的地方 -->
      <div ref="containerRef" class="canvas-container"></div>
      
      <!-- 工具栏:控制画布的按钮组 -->
      <div class="toolbar">
        <button @click="zoomOut">缩小</button>
        <button @click="zoomIn">放大</button>
        <button @click="resetZoom">重置</button>
        <button @click="undo">撤销</button>
        <button @click="redo">重做</button>
        <button @click="download">下载图片</button>
      </div>
    </div>
  </div>
</template>

2. 数据定义部分

containerRef:获取DOM元素的引用,用于挂载画布
graph:存储X6画布实例,后续所有操作都通过它进行
nodeTypes:定义可用的节点类型,包括形状、大小和显示名称

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { Graph, Shape } from '@antv/x6'

// 画布容器引用
const containerRef = ref<HTMLElement>()
// 画布实例
let graph: Graph

// 节点类型定义:供左侧节点库使用
const nodeTypes = [
  { type: 'rect', label: '矩形节点', width: 100, height: 60 },
  { type: 'circle', label: '圆形节点', width: 60, height: 60 },
  { type: 'ellipse', label: '椭圆节点', width: 80, height: 50 },
  { type: 'diamond', label: '菱形节点', width: 80, height: 80 }
]

3. 画布初始化部分

创建X6画布实例,配置各项功能

// 初始化画布
onMounted(() => {
  if (!containerRef.value) return
  
  graph = new Graph({
    container: containerRef.value,
    width: 800,
    height: 600,
    // 背景设置
    background: {
      color: '#f5f5f5'  // 背景颜色
    },
    // 网格设置
    grid: {
      visible: true,  // 显示网格
      type: 'doubleMesh',  // 双网格效果
      args: [
        { color: '#e9e9e9', thickness: 1 },  // 主网格
        { color: '#c9c9c9', thickness: 1, factor: 5 }  // 次网格
      ]
    },
    // 画布拖拽设置
    panning: {
      enabled: true,  // 启用画布拖拽
      modifiers: 'ctrl'  // 按住ctrl键才能拖拽画布
    },
    // 滚轮缩放设置
    mousewheel: {
      enabled: true,
      modifiers: 'ctrl',  // 按住ctrl键才能缩放
      minScale: 0.5,  // 最小缩放比例
      maxScale: 2     // 最大缩放比例
    },
    // 连线设置
    connecting: {
      router: 'manhattan',  // 连线路径算法
      connector: {  // 连线样式
        name: 'rounded',
        args: { radius: 8 }
      },
      snap: { radius: 20 },  // 吸附距离
      createEdge() {  // 创建连线的默认样式
        return new Shape.Edge({
          attrs: {
            line: {
              stroke: '#1890ff',
              strokeWidth: 2,
              targetMarker: { name: 'classic', size: 8 }
            }
          }
        })
      }
    }
  })

  // 注册自定义节点
  registerCustomNodes()
  // 绑定事件
  bindEvents()
})

4. 注册自定义节点

// 注册自定义节点
const registerCustomNodes = () => {
  // 矩形节点
  Graph.registerNode('custom-rect', {
    inherit: 'rect',  // 继承自基础矩形
    width: 100,
    height: 60,
    attrs: {
      body: {  // 节点主体样式
        stroke: '#1890ff',  // 边框颜色
        strokeWidth: 2,      // 边框宽度
        fill: '#ffffff',     // 填充色
        rx: 6, ry: 6        // 圆角半径
      },
      label: {  // 节点标签样式
        text: '节点',
        fill: '#333',
        fontSize: 14,
        fontWeight: 'bold'
      }
    }
  })

  // 圆形节点
  Graph.registerNode('custom-circle', {
    inherit: 'circle',
    width: 60,
    height: 60,
    attrs: {
      body: {
        stroke: '#52c41a',
        strokeWidth: 2,
        fill: '#ffffff'
      },
      label: {
        text: '节点',
        fill: '#333',
        fontSize: 14
      }
    }
  })
}

5. 事件绑定部分

// 绑定事件
const bindEvents = () => {
  // 节点点击事件
  graph.on('node:click', ({ node }) => {
    console.log('节点被点击:', node)
  })

  // 节点双击事件 - 修改节点名称
  graph.on('node:dblclick', ({ node }) => {
    const newLabel = prompt('修改节点名称', node.attr('label/text'))
    if (newLabel) {
      node.attr('label/text', newLabel)
    }
  })

  // 空白区域点击
  graph.on('blank:click', () => {
    console.log('空白区域被点击')
  })

  // 连线创建成功
  graph.on('edge:connected', ({ edge }) => {
    console.log('连线创建成功:', edge)
  })
}

6. 拖拽功能实现

handleDragStart:当从左侧节点库拖拽时,保存节点信息
drop事件:当释放鼠标时,在对应位置创建新节点
坐标转换:将鼠标屏幕坐标转换为画布坐标

// 拖拽开始
const handleDragStart = (e: DragEvent, node: any) => {
  // 存储节点数据到拖拽事件
  e.dataTransfer?.setData('text/plain', JSON.stringify(node))
  e.dataTransfer?.setData('nodeType', node.type)
  e.dataTransfer!.effectAllowed = 'move'
}

// 监听放置事件
onMounted(() => {
  if (!containerRef.value) return
  
  // 允许拖拽元素放置
  containerRef.value.addEventListener('dragover', (e) => {
    e.preventDefault()
  })

  // 处理放置事件
  containerRef.value.addEventListener('drop', (e) => {
    e.preventDefault()
    
    const data = e.dataTransfer?.getData('text/plain')
    if (!data) return
    
    const nodeData = JSON.parse(data)
    
    // 将鼠标坐标转换为画布坐标
    const point = graph.clientToLocal(e.clientX, e.clientY)
    
    // 在鼠标位置创建新节点
    const node = graph.addNode({
      shape: nodeData.type === 'rect' ? 'custom-rect' : 
             nodeData.type === 'circle' ? 'custom-circle' : 'rect',
      x: point.x - nodeData.width / 2,  // 居中放置
      y: point.y - nodeData.height / 2,
      width: nodeData.width,
      height: nodeData.height,
      label: nodeData.label,
      attrs: {
        label: { text: nodeData.label }
      }
    })
  })
})

7. 工具栏功能

// 缩放功能
const zoomIn = () => {
  const scale = graph.zoom()
  graph.zoom(scale + 0.1, { absolute: true })
}

const zoomOut = () => {
  const scale = graph.zoom()
  graph.zoom(scale - 0.1, { absolute: true })
}

const resetZoom = () => {
  graph.zoom(1)  // 重置到原始大小
}

// 历史操作
const undo = () => {
  graph.undo()  // 撤销
}

const redo = () => {
  graph.redo()  // 重做
}

// 导出图片
const download = () => {
  graph.toPNG((dataUrl) => {
    const link = document.createElement('a')
    link.download = 'graph.png'
    link.href = dataUrl
    link.click()
  })
}

// 清理画布
onUnmounted(() => {
  if (graph) {
    graph.dispose()  // 释放资源
  }
})