自定义节点

阅读时间约 4 分钟

我们在 X6 中内置了一些基础图形,如 RectCircleEllipse 等,但这些还远远不能满足我们的实际需求,我们需要能够定义具有业务意义的节点,例如 ER 图的表格节点。自定义节点也不是什么难事,其实就是在组合使用 SVG 中的 <rect><circle><ellipse><image><text><path> 等基础元素,如果你对这些基础元素还不熟悉,可以参考 MDN 提供的教程,使用这些基础元素可以定义出任何我们想要的图形。

原理

自定义节点实际上是从基础节点派生(继承)出我们自己的节点,并覆盖基类的某些选项和方法。

三步法

以内置节点 Rect 为例,自定义节点可以分以下三步走。

第一步:继承

import { Node } from '@antv/x6'

class Rect extends Node { 
  // 省略实现细节
}

第二步:配置

调用继承的静态方法 config(options) 来配置节点选项的默认值、自定义选项自定义属性,最常用选项的是通过 markup 来指定节点默认的 SVG/HTML 结构,通过 attrs 来指定节点的默认属性样式。

名称类型是否必选默认值说明
propHooksFunction | Function[] | Objectundefined自定义选项钩子。
attrHooksObjectundefined自定义属性钩子。
...othersObject节点选项

看下面 Rect 节点的默认配置。

Rect.config({
  width: 100,
  height: 40,
  markup: [
    {
      tagName: 'rect',
      selector: 'body',
    },
    {
      tagName: 'text',
      selector: 'label',
    },
  ],
  attrs: {
    body: {
      fill: '#ffffff',
      stroke: '#333333',
      strokeWidth: 2,
    },
    label: {
      fontSize: 14,
      fill: '#333333',
      refX: '50%',
      refY: '50%',
      textAnchor: 'middle',
      textVerticalAnchor: 'middle',
    },
  },
  // 通过钩子将自定义选项 label 应用到 'attrs/text/text' 属性上
  propHooks(metadata) {
    const { label, ...others } = metadata
    if (label) {
      ObjectExt.setByPath(others, 'attrs/text/text', label)
    }
    return others
  },
})

上面代码中,我们通过 widthheight 指定了节点的默认大小,然后通过 markup 定义了节点由 <rect><text> 两个 SVG 元素构成,并分别指定了 bodylabel 两个选择器,接着就可以在 attrs 中通过这两个选择器来指定节点的默认样式。最后通过 propHooks 定义了一个自定义选项 label,这样我们就可以通过 label 设置标签文本。

第三步:注册

调用 Graph 的静态方法 registerNode 来注册节点,注册以后就可以像使用内置节点那样来使用节点。

Graph.registerNode(name: string, cls: typeof Node, overwrite?: boolean)
参数名类型是否必选默认值说明
nameString注册的节点名。
clstypeof Node节点类,直接或间接继承 Node 的类。
overwriteBooleanfalse重名时是否覆盖,默认为 false 不覆盖(重名时报错)。

例如,注册名为 'rect' 的节点。

Graph.registerNode('rect', Rect)

注册以后,我们可以像下面这样来使用。

graph.addNode({
  shape: 'rect',
  x: 30,
  y: 40,
})

便捷方法一

有时候我们可能在继承节点后并不需要任何扩展任何方法,而只是覆盖某些默认样式。例如,定义一个红色边框的矩形。

class RedRect extends Rect { }

// 覆盖默认边框颜色
RedRect.config({
  attrs: {
    body: {
      stroke: 'red',
    },
  },
})

上面第一行代码就显得有点尴尬:实现了继承但没有扩展任何方法,有点大材小用的感觉。所以我们也提供了一个更加便捷的静态方法 define 来定义这类节点。

const RedRect = Rect.define({
  attrs: {
    body: {
      stroke: 'red',
    },
  },
})

Graph.registerNode('red-rect', RedRect)

该方法将其调用者(如上面的 Rect)作为基类,继承出一个新的节点,然后调用新节点的静态方法 config 来配置默认选项。

需要注意的是,上面代码生成的 RedRect 类的类名并不是 'RedRect',而是系统自动生成的类名,当指定 constructorName 选项后,其大驼峰(CamelCase)形式将作为新节点的类名。

const RedRect = Rect.define({
  constructorName: 'red-rect',
  attrs: {
    body: {
      stroke: 'red',
    },
  },
})

Graph.registerNode('red-rect', RedRect)

如果我们提供了 shape 选项,那么系统将自动为你注册节点。当没有指定 constructorName 选项时,shape 的大驼峰形式(CamelCase)也将作新节点的类名,也就是说下面代码定义的节点类名为 'RedRect'

Rect.define({
  shape: 'red-rect', // 自动注册名为 'red-rect' 的节点,并且节点类名为 'RedRect'。
  attrs: {
    body: {
      stroke: 'red',
    },
  },
})

除了 constructorNameshape 两个特殊选项外,其他选项都与 config 方法的选项保持一致。下表是 define 方法支持的选项。

名称是否必选类型说明
constructorNameString类名。
shapeString自动注册的节点名,当 constructorName 缺省时其大驼峰(CamelCase)形式也将作为类名。
...othersObjectconfig 方法的选项。

便捷方法二

上面提到的 Graph.registerNode 方法还有另外一种签名,使用该方法可以同时实现定义和注册节点。

Graph.registerNode(name: string, options: Object, overwrite?: boolean)
参数名类型是否必选默认值说明
nameString注册的节点名。
optionsObject选项。
overwriteBooleanfalse重名时是否覆盖,默认为 false 不覆盖(重名时报错)。

通过 options.inherit 来指定继承的基类,默认值为 Node 类,支持字符串或节点类,当 options.inherit 为字符串时将自动从已注册的节点中找到对应的节点作为基类,options 的其他选项与 define 方法一致。当 options.constructorName 类名缺省时,第一个参数 name 的大驼峰形式(CamelCase)也将作为自定义节点的类名。

Graph.registerNode('red-rect', {
  inherit: Rect, // 或 'rect'
  attrs: {
    body: {
      stroke: 'red',
    },
  },
})

案例

接下来我们就基于 Shape.Rect 来自定义一个矩形 CustomRect,这里不修改矩形的 markup 定义,而仅仅是做样式覆盖。

使用便捷方法一。

import { Shape } from '@antv/x6'

Shape.Rect.define({
  shape: 'custom-rect',
  width: 300, // 默认宽度
  height: 40, // 默认高度
  attrs: {
    body: {
      rx: 10, // 圆角矩形
      ry: 10,
      strokeWidth: 1,
      fill: '#5755a1',
      stroke: '#5755a1',
    },
    label: {
      fill: '#fff',
      fontSize: 18,
      refX: 10, // x 轴偏移,类似 css 中的 margin-left
      textAnchor: 'left', // 左对齐
    }
  },
})

使用便捷方法二。

Graph.registerNode('custom-rect', {
  inherit: 'rect', // 继承自 Shape.Rect
  width: 300, // 默认宽度
  height: 40, // 默认高度
  attrs: {
    body: {
      rx: 10, // 圆角矩形
      ry: 10,
      strokeWidth: 1,
      fill: '#5755a1',
      stroke: '#5755a1',
    },
    label: {
      fill: '#fff',
      fontSize: 18,
      refX: 10, // x 轴偏移,类似 css 中的 margin-left
      textAnchor: 'left', // 左对齐
    }
  },
})

然后我们可以这样来使用。

graph.addNode({
  x: 100,
  y: 60,
  shape: 'custom-rect',
  label: 'My Custom Rect', // label 继承于基类的自定义选项
});