antv-x6图编辑引擎使用及踩坑分享

9,468 阅读15分钟

〇. 概述

antv-x6 是基于 HTMLSVG 的图编辑引擎,提供低成本的定制能力和开箱即用的内置扩展,方便我们快速搭建 DAG (有向无环图)图、ER (实体联系模式图) 图、流程图、血缘图等应用。

✨ 特性:

  • 🌱 极易定制:支持使用SVG/HTML/React/Vue定制节点样式和交互;
  • 🚀 开箱即用:内置 10+ 图编辑配套扩展,如框选、对齐线、小地图等;
  • 🧲 数据驱动:基于MVC架构,用户更加专注于数据逻辑和业务逻辑;
  • 💯 事件驱动:可以监听图表内发生的任何事件。

本文档旨在使读者了解 antv-x6 常见用途及用法, 详细内容请参考官方文档:

antv-x6 官方文档: x6.antv.antgroup.com/

SVG 图像入门教程: www.ruanyifeng.com/blog/2018/0…

SVG - MDN: developer.mozilla.org/zh-CN/docs/…

antv-x6 使用布局: x6.antv.antgroup.com/temp/layout

一. 快速上手

1. 安装

npm install @antv/x6 --save

2. 初始化画布

在页面中创建一个画布容器,然后初始化画布对象,可以通过配置设置画布的样式,比如背景颜色。

<div id="container"></div>
// ...
import { Graph } from "@antv/x6";
// ...
const graph = new Graph({
  container: document.getElementById("container"),
  width: 800,
  height: 600,
  background: {
    color: "#F2F7FA", // 指定画布背景色
  },
});
// ...

3. 注册VUE节点

X6 支持使用Vue 组件来渲染节点

// 子组件 VueNode.vue
<template>
  <div class="node">
    <div class="text">{{nodeData.text}}</div>
  </div>
</template>

<script>
  export default {
    name: "vueNode",
    inject: ["getNode"], // x6使用依赖注入向vue节点传递数据
    data() {
      return {
        nodeData: {},
      };
    },
    created() {
      this.nodeData = this.getNode()?.store?.data;
    },
  };
</script>

<style lang="less" scoped>
  div {
    box-sizing: border-box;
  }
  .node {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 200px;
    height: 90px;
    background: #ffffff;
    box-shadow: 0px 3px 6px 0px rgba(213, 224, 243, 0.5);
    border-radius: 5px;
    overflow: hidden;
    border: 2px solid #dcdfe6;
    .text {
      font-size: 20px;
      color: rebeccapurple;
    }
  }
</style>
// 父组件
// ...
import { register } from "@antv/x6-vue-shape";
import VueNode from "../components/VueNode.vue";
// ...
register({
  shape: "vueNode",
  component: VueNode,
});
// ...

4. 渲染节点和边

X6支持使用fromJSON()渲染JSON格式数据,该对象中nodes代表节点数据,edges代表边数据。

// ...
data () {
    return {
      graphData: {
        nodes: [
          {
            id: 'node1',
            shape: 'rect',
            x: 40,
            y: 40,
            width: 100,
            height: 40,
            label: 'hello',
            attrs: {
              // body 是选择器名称,选中的是 rect 元素
              body: {
                stroke: '#8f8f8f',
                strokeWidth: 1,
                fill: '#fff',
                rx: 6,
                ry: 6
              }
            }
          },
          {
            id: 'node2',
            shape: 'rect',
            x: 160,
            y: 180,
            width: 100,
            height: 40,
            label: 'world',
            attrs: {
              body: {
                stroke: '#8f8f8f',
                strokeWidth: 1,
                fill: '#fff',
                rx: 6,
                ry: 6
              }
            }
          },
          {
            id: 'node3',
            shape: 'vueNode', // 支持渲染vue节点
            x: 160,
            y: 280,
            text: 'helloVue'
          }
        ],
        edges: [
          {
            shape: 'edge',
            source: 'node1',
            target: 'node2',
            label: 'x6',
            attrs: {
              // line 是选择器名称,选中的边的 path 元素
              line: {
                stroke: '#8f8f8f',
                strokeWidth: 1
              }
            }
          },
          {
            shape: 'edge',
            source: 'node2',
            target: 'node3',
            attrs: {
              // line 是选择器名称,选中的边的 path 元素
              line: {
                stroke: '#8f8f8f',
                strokeWidth: 1
              }
            }
          }
        ]
      }, // 画布数据
    }
  }

// ...

graph.fromJSON(this.graphData) // 渲染元素

二. SVG基础

antv-x6 是基于HTMLSVG的图编辑引擎 参考文档:

SVG 图像入门教程:www.ruanyifeng.com/blog/2018/0…

SVG - MDN: developer.mozilla.org/zh-CN/docs/…

1. 概述

SVG是一种基于XML语法的图像格式,全称是可缩放矢量图Scalable Vector Graphics。其他图像格式都是基于像素处理的,SVG 则是属于对图像的形状描述,所以它本质上是文本文件,体积较小,且不管放大多少倍都不会失真。

SVG文件可以直接插入网页,成为DOM的一部分,然后用JavaScriptCSS进行操作。

<!DOCTYPE html>
<html>
<head></head>
<body>
<svg
  id="mysvg"
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 800 600"
  preserveAspectRatio="xMidYMid meet"
>
  <circle id="mycircle" cx="400" cy="300" r="50" />
<svg>
</body>
</html>

SVG 代码也可以写在一个独立文件中,然后用<img><object><embed><iframe>等标签插入网页。

<img src="circle.svg" />
<object id="object" data="circle.svg" type="image/svg+xml"></object>
<embed id="embed" src="icon.svg" type="image/svg+xml" />
<iframe id="iframe" src="icon.svg"></iframe>

CSS也可以使用SVG文件。

.logo {
  background: url(icon.svg);
}

SVG文件还可以转为BASE64编码,然后作为Data URI写入网页。

<img src="data:image/svg+xml;base64,[data]" />

2. 语法

<svg>标签

<svg width="100%" height="100%">
  <circle id="mycircle" cx="50" cy="50" r="50" />
</svg>

image.png

<svg>width属性和height属性,指定了SVG图像在HTML元素中所占据的宽度和高度。除了相对单位,也可以采用绝对单位(单位:像素)。如果不指定这两个属性,SVG图像默认大小是 300 像素(宽) x 150 像素(高)。 如果只想展示SVG 图像的一部分,就要指定viewBox属性

<svg width="100" height="100" viewBox="50 50 50 50">
  <circle id="mycircle" cx="50" cy="50" r="50" />
</svg>

image.png <viewBox>属性的值有四个数字,分别是左上角的横坐标和纵坐标、视口的宽度和高度。上面代码中,SVG图像是 100 像素宽 x 100 像素高,viewBox属性指定视口从(50, 50)这个点开始。所以,实际看到的是右下角的四分之一圆

注意,视口必须适配所在的空间。上面代码中,视口的大小是 50 x 50,由于SVG图像的大小是 100 x 100,所以视口会放大去适配SVG图像的大小,即放大了四倍。

<circle>标签

<circle>标签代表圆形。

<svg width="300" height="180">
  <circle cx="30" cy="50" r="25" />
  <circle cx="90" cy="50" r="25" class="red" />
  <circle cx="150" cy="50" r="25" class="fancy" />
</svg>

上面的代码定义了三个圆。<circle>标签的cxcyr属性分别为横坐标、纵坐标和半径,单位为像素。坐标都是相对于<svg>画布的左上角原点。

class属性用来指定对应的CSS类。

.red {
  fill: red;
}

.fancy {
  fill: none;
  stroke: black;
  stroke-width: 3pt;
}

image.png

SVGCSS属性与网页元素有所不同。

  • fill:填充色
  • stroke:描边色
  • stroke-width:边框宽度

<rect>标签

标签用于绘制矩形。

<svg width="300" height="180">
  <rect
    x="0"
    y="0"
    height="100"
    width="200"
    style="stroke: #70d5dd; fill: #dd524b"
  />
</svg>

image.png

<rect>x属性和y属性,指定了矩形左上角端点的横坐标和纵坐标;width属性和height属性指定了矩形的宽度和高度(单位像素)。

<path>标签

<svg width="300" height="180">
  <path
    d="
  M 18,3
  L 46,3
  L 46,40
  L 61,40
  L 32,68
  L 3,40
  L 18,40
  Z
"
  ></path>
</svg>

image.png

<path>d属性表示绘制顺序,它的值是一个长字符串,每个字母表示一个绘制动作,后面跟着坐标。

  • M:移动到(moveto)
  • L:画直线到(lineto)
  • Z:闭合路径

其他常用标签

除了上诉的标签, SVG还支持以下常用标签

<line>标签用来绘制直线
<polyline>标签用于绘制一根折线
<ellipse>标签用于绘制椭圆
<polygon>标签用于绘制多边形
<text>标签用于绘制文本
<use>标签用于复制一个形状
<g>标签用于将多个形状组成一个组(group)
<defs>标签用于自定义形状,它内部的代码不会显示
<pattern>标签用于自定义一个形状,该形状可以被引用来平铺一个区域
<image>标签用于插入图片文件
<animate>标签用于产生动画效果
<animate>标签对 CSS 的 transform 属性不起作用,如果需要变形,就要使用<animateTransform>标签

三. x6 基础概念

  1. 安装及相关插件

$ npm install @antv/x6 --save

2.x版本将部分功能拆分为插件形式, 请按需安装:

  @antv/x6-plugin-clipboard // 剪切板功能
  @antv/x6-plugin-history // 撤销重做功能
  @antv/x6-plugin-keyboard // 快捷键功能
  @antv/x6-plugin-minimap // 小地图功能
  @antv/x6-plugin-scroller // 滚动画布功能
  @antv/x6-plugin-selection // 框选功能
  @antv/x6-plugin-snapline // 对齐线功能
  @antv/x6-plugin-dnd //  dnd 功能
  @antv/x6-plugin-stencil //  stencil 功能
  @antv/x6-plugin-transform // 图形变换功能
  @antv/x6-plugin-export // 图片导出功能
  @antv/x6-react-components // 配套 UI 组件
  @antv/x6-react-shape //  react 渲染功能
  @antv/x6-vue-shape //  vue 渲染功能 需与x6使用同一个大版本
  @antv/layout // 布局功能 x6可以使用antv公共的布局库
  1. 画布

X6 中,画布Graph 是图的载体,它包含了图上的所有元素(节点、边等),同时挂载了图的相关操作(如交互监听、元素操作、渲染等)。

初始化画布:

在页面中创建一个用于容纳 X6 绘图的容器,可以是一个 div 标签

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

创建一个 Graph 对象,并为其指定一个页面上的绘图容器,通常也会指定画布的大小

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

const graph = new Graph({
  container: document.getElementById("container"),
  width: 800,
  height: 600,
  background: {
    color: "#F2F7FA",
  },
});

如果希望画布对象撑满父容器, 可以使用autoResize

<!-- 注意,使用 autoResize 配置时,需要在画布容器外面再套一层宽高都是 100% 的外层容器,在外层容器上监听尺寸改变,当外层容器大小改变时,画布自动重新计算宽高以及元素位置。 -->
<div style="width:100%; height:100%">
  <div id="container"></div>
</div>
const graph = new Graph({
  container: document.getElementById("container"),
  autoResize: true,
});

创建 Graph 对象时可以通过  background  和  grid  两个配置来设置画布的背景以及网格

// ...
const graph = new Graph({
            ...,
           background: {
        color: '#F2F7FA',
      },
      grid: {
        visible: true,
        type: 'doubleMesh',
        args: [
          {
            color: '#eee', // 主网格线颜色
            thickness: 1, // 主网格线宽度
          },
          {
            color: '#ddd', // 次网格线颜色
            thickness: 1, // 次网格线宽度
            factor: 4, // 主次网格线间隔
          },
        ],
      },
})
// ...

画布的拖拽、缩放也是常用操作,Graph 中通过  panning  和  mousewheel  配置来实现这两个功能,鼠标按下画布后移动时会拖拽画布,滚动鼠标滚轮会缩放画布

const graph = new Graph({
  ...,
  // 属性值可以时布尔值表示开启功能
  panning: true, // 开启画布拖拽
  // 也可以是详细配置的对象
  mousewheel: {
        enabled: true, // 开启滚轮缩放交互
        zoomAtMousePosition: false, // 是否将鼠标位置作为中心缩放
        modifiers: ['ctrl', 'meta'] // 修饰键
  },
})

X6最吸引开发者的地方是具备非常完整的交互定制能力, 可以通过这些属性选择开启或关闭它们

const graph = new Graph({
  ...,
  interacting: {
    nodeMovable: false, // 节点是否可以被移动
    magnetConnectable: false, // 当在具有 magnet 属性的元素上按下鼠标开始拖动时,是否触发连线交互
    edgeMovable: false, // 边是否可以被移动
    edgeLabelMovable: false, // 边的标签是否可以被移动
    arrowheadMovable: false, // 边的起始/终止箭头(在使用 arrowhead 工具后)是否可以被移动
    vertexMovable: false, // 边的路径点是否可以被移动
    vertexAddable: false, // 是否可以添加边的路径点
    vertexDeletable: false // 边的路径点是否可以被删除
  }
})

  1. 节点

X6 支持使用 SVGHTML 来渲染节点内容,在此基础上,我们还可以使用 ReactVue 组件来渲染节点,这样在开发过程中会非常便捷。在拿到设计稿之后,你就需要权衡一下使用哪一种渲染方式,可以参考下面的一些建议:

  • 如果节点内容比较简单,而且需求比较固定,使用 SVG 节点
  • 其他场景,都推荐使用当前项目所使用的框架来渲染节点

React/Vue/HTML 渲染方式也存在一些限制,因为浏览器的兼容性问题,有时会出现一些异常的渲染行为。主要表现形式为节点内容展示不全或者节点内容闪烁。可以通过一些方法规避,比如在节点内部元素的 css 样式中不要使用  position:absoluteposition:relativetranformopacity

向画布添加节点

graph.addNode({
  shape: "rect",
  x: 100,
  y: 40,
  width: 100,
  height: 40,
});

节点支持以下常用属性:

属性名类型默认值描述
idstring节点的唯一标识,推荐使用具备业务意义的 ID, 默认使用自动生成的 UUID。
shapestring渲染节点/边的图形。节点对应的默认值为  rect
markupMarkup节点/边的 SVG/HTML 片段。
attrsAttr.CellAttrs节点/边属性样式。类比css
xnumber0节点位置 x 坐标,单位为 px。
ynumber0节点位置 y 坐标,单位为 px。
widthnumber1节点宽度,单位为 px。
heightnumber1节点高度,单位为 px。
自定义的属性any自定义业务数据

上面使用  shape  来指定了节点的图形,X6 内置节点与  shape  名称对应关系如下表

构造函数shape 名称描述
Shape.Rectrect矩形。
Shape.Circlecircle圆形。
Shape.Ellipseellipse椭圆。
Shape.Polygonpolygon多边形。
Shape.Polylinepolyline折线。
Shape.Pathpath路径。
Shape.Imageimage图片。
Shape.HTMLhtmlHTML 节点,使用 foreignObject 渲染 HTML 片段。
  1. 使用 vue 节点

使用 vue 节点时的数据及事件交互:

vue节点组件可以通过注入x6提供的getNode获得节点对象, 并通过调用父组件提供的方法传递数据

子组件:

<template>
  <div class="node">
    <div class="text">{{nodeData.text}}</div>
    <input type="text" v-model="inputText" />
    <button @click="btnClick">click me</button>
  </div>
</template>

<script>
  export default {
    name: "x6vueNode",
    inject: ["getNode"], // x6使用依赖注入向vue节点传递数据
    data() {
      return {
        nodeData: {},
        inputText: "",
      };
    },
    methods: {
      btnClick() {
        this.nodeData.foo(this.inputText); // 调用父组件方法
      },
    },
    created() {
      this.nodeData = this.getNode()?.store?.data;
    },
  };
</script>
<style lang="less" scoped>
  div {
    box-sizing: border-box;
  }

  .node {
    display: flex;
    flex-flow: column;
    justify-content: center;
    align-items: center;
    width: 200px;
    height: 90px;
    background: #ffffff;
    box-shadow: 0px 3px 6px 0px rgba(213, 224, 243, 0.5);
    border-radius: 5px;
    overflow: hidden;
    border: 2px solid #dcdfe6;
    input {
      width: 60%;
      border: solid 2px #666;
      border-radius: 5px;
    }
    .text {
      font-size: 20px;
      color: rebeccapurple;
    }
  }
</style>

父节点:

import { register } from "@antv/x6-vue-shape";
import VueNode from "../components/VueNode.vue";
// ...
// 注册vue节点
register({
  shape: "vueNode",
  component: VueNode,
});
// ...
// 添加vue节点:
this.graph.addNode({
  id: "node3",
  shape: "X6VueNode", // 支持渲染vue节点
  x: 160,
  y: 280,
  text: "helloVue",
  foo: this.foo, // 向节点暴露的方法
});

向画布添加边

graph.addEdge({
  shape: "edge",
  source: "node1",
  target: "node2",
});

边支持以下常用属性:

属性名类型描述
idstring边的唯一标识,推荐使用具备业务意义的 ID, 默认使用自动生成的 UUID。
shapestring渲染节点/边的图形。节点对应的默认值为  rect
markupMarkup节点/边的 SVG/HTML 片段。
attrsAttr.CellAttrs节点/边属性样式。类比css。可用来定制箭头和边的样式。
sourceTerminalData源节点或起始点。
targetTerminalData目标节点或目标点。
verticesPoint.PointLike[]路径点。
routerRouterData路由。
connectorConnectorData连接器。
labelsLabel[]标签。
defaultLabelLabel默认标签。

边的源和目标节点支持以下属性

source: rect1, // 源节点对象
source: 'rect1', // 源节点 ID
source: { cell: rect1, port: 'out-port-1' }, // 源节点和连接桩 ID
source: { x: 100, y: 120 } // 坐标点

边支持添加路径点vertices。边从起始点开始,按顺序经过路径点,最后到达终止点

graph.addEdge({
  source: rect1,
  target: rect2,
  vertices: [
    { x: 100, y: 200 },
    { x: 300, y: 120 },
  ],
});

路由  router  将对  vertices  进一步处理,并在必要时添加额外的点,然后返回处理后的点

例如,经过orth路由处理后,边的每一条链接线段都是水平或垂直的

graph.addEdge({
  source: rect1,
  target: rect2,
  vertices: [
    { x: 100, y: 200 },
    { x: 300, y: 120 },
  ],
  // 如果没有 args 参数,可以简写为 router: 'orth'
  router: {
    name: "orth",
    args: {},
  },
});

X6默认提供了以下几种路由:

路由名称说明图例
normal默认路由,原样返回路径点。
orth正交路由,由水平或垂直的正交线段组成。
oneSide受限正交路由,由受限的三段水平或垂直的正交线段组成。
manhattan智能正交路由,由水平或垂直的正交线段组成,并自动避开路径上的其他节点(障碍)。
metro智能地铁线路由,由水平或垂直的正交线段和斜角线段组成,类似地铁轨道图,并自动避开路径上的其他节点(障碍)。
er实体关系路由,由 Z 字形的斜角线段组成。

连接器  connector  将路由  router  返回的点加工成渲染边所需要的pathData。例如,rounded  连接器将连线之间的倒角处理为圆弧倒角

graph.addEdge({
  source: rect1,
  target: rect2,
  vertices: [
    { x: 100, y: 200 },
    { x: 300, y: 120 },
  ],
  router: "orth",
  // 如果没有 args 参数,可以简写写 connector: 'rounded'
  connector: {
    name: "rounded",
    args: {},
  },
});

X6默认提供了以下几种连接器

连接器说明图例
normal简单连接器,用直线连接起点、路由点和终点。
smooth平滑连接器,用三次贝塞尔曲线线连接起点、路由点和终点。
rounded圆角连接器,用直线连接起点、路由点和终点,并在线段连接处用圆弧链接(倒圆角)。
jumpover跳线连接器,用直线连接起点、路由点和终点,并在边与边的交叉处用跳线符号链接。

labels 用于设置标签文本、位置、样式等。通过数组形式支持多标签

const edge = graph.addEdge({
  source: rect1,
  target: rect2,
  labels: ["edge"], // 通过 labels 可以设置多个标签
});
  1. 连接桩

连接桩可以用于边的源节点和目标点, 可以以优雅的交互添加边链接节点

  1. 布局

x6 可以使用 antv 公共的布局库 @antv/layout

文档: x6.antv.antgroup.com/temp/layout

安装布局插件:

npm install @antv/layout --save

可以使用GridLayout对节点数据进行处理, GridLayout使用相应算法为节点添加坐标

// ...
import { DagreLayout } from "@antv/layout";
// ...
// 布局所需要的数据格式(需要确定节点的大小, 不需要节点xy坐标)
const model = {
  nodes: [
    {
      id: "node1",
      size: {
        width: 30,
        height: 40,
      },
    },
    {
      id: "node2",
      size: {
        width: 30,
        height: 40,
      },
    },
  ],
  edges: [
    {
      source: "node1",
      target: "node2",
    },
  ],
};
const gridLayout = new DagreLayout({
  type: "dagre", // 布局类型
  align: undefined, // 节点对齐方式
  begin: [0, 0], // 布局左上角对齐位置
  rankdir: "TB", // 布局的方向
  nodesep: "180", // 节点间距
  ranksep: "60", // 层间距
});
const newModel = gridLayout.layout(model);
graph.fromJSON(newModel);

常用的布局类型:

  • 网格布局: grid

  • 环形布局: circular

  • 层次布局: dagre

每种布局创建时传入的配置不同, 具体请参考文档: x6.antv.antgroup.com/temp/layout

  1. 常用 API

可以直接用的:

graph.centerContent(); // 画布居中
graph.zoom(0.2); // 画布缩放
graph.zoomTo(1); // 画布缩放到
graph.zoomToFit({ maxScale: 2, padding: 20 }); // 撑满画布, maxScale 配置最大缩放级别, padding 配置内边距
graph.fromJSON(jsonData); // 使用json数据渲染画布数据
graph.toJSON(); // 将节点/边的结构化数据转换为 JSON 数据

撤销和重做:

graph.undo(); // 撤销
graph.redo(); // 重做

通过history:change事件监听画布操作, 控制是否可以撤销 | 重做

this.graph.on("history:change", () => {
  this.state = {
    canRedo: this.graph.canRedo(),
    canUndo: this.graph.canUndo(),
  };
});

导出 svg :

graph.exportSVG(fileName, options); // 导出svg

fileName  为文件名称,缺省为  chartoptions  描述如下:

属性名类型默认值描述
preserveDimensionsbooleanSizepreserveDimensions 用来控制导出 svg 的尺寸, 如果不设置,width 和 height 默认为 100%;如果设置为 true, width 和 height 会自动计算为图形区域的实际大小
viewBoxRectangle.RectangleLike-设置导出 svg 的 viewBox
copyStylesbooleantrue是否复制外部样式表中的样式,默认是 true。开启 copyStyles 后,在导出过程中因为需要禁用所有样式表,所以页面可能会出现短暂的样式丢失现象。如果效果特别差,可以将 copyStyles 设置为 false (亲测即使开启 copyStyles 也会丢失样式, 可配合下面的 stylesheet 选项解决)
stylesheetstring-自定义样式表 (亲测支持 css 扩展语言, 譬如 less)
serializeImagesbooleantrue是否将 image 元素的 xlink:href 链接转化为 dataUri 格式
beforeSerialize(this: Graph, svg: SVGSVGElement) => any-可以在导出 svg 字符串之前调用 beforeSerialize 来修改它

经实测发现exportSVG仅会导出可见部分画布, 所以导出前可能需要将画布缩放到全部可见: graph.zoomToFit()

导出图片:

graph.exportPNG(fileName, options); // 导出png
graph.exportJPEG(fileName, options); // 导出jpg

fileName 为文件名称, 缺省为 chart, options 除了继承上面exportSVGoptions外还支持以下属性:

属性名类型默认值描述
widthnumber-导出图片的宽度
heightnumber-导出图片的高度
backgroundColorstring-导出图片的背景色
paddingNumberExt.SideOptions-图片的 padding
qualitynumber-图片质量,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92

经实测发现exportPNGexportJPEG会导出画布全部部分, 但清晰度和当前页面展示的一致, 所以导出前可能需要将画布缩放到清晰的倍数: graph.zoomTo(1)