[day4] 我的元件我做主~ 创建适用于 X6 的自定义元件库

1,517 阅读3分钟

这是我参与更文挑战的第 4 天,活动详情查看:更文挑战


首先还是简单介绍一下 X6

X6 是图编辑引擎,特点是节点、边、等元素的定制能力非常强,经常用来构建流程图、ER 图、DAG 图、脑图等应用。

根据官网的示例,X6 可以实现以下的效果

image.png

具体示例,请看这里:x6.antv.vision/zh/examples…

什么是元件?

节点和边统称为元件。元件是节点和边的共同基类。

元件定义了节点和边共同属性和方法,如属性样式、可见性、业务数据等,并且在实例化、定制样式、配置默认选项等方面具有相同的行为。

看下面的继承关系:

graph LR
Cell --> Node
Cell --> Edge

Node --> Shape.Rect       
Node --> Shape.Circle    
Node --> Shape.Ellipse    
Node --> Shape.Xxx...    

Edge --> Shape.Edge
Edge --> Shape.DoubleEdge
Edge --> Shape.ShadowEdge

内置元件

官方内置了一批节点和边,具体看这里:内置节点

image.png

image.png

除了上面提到的内置元件之外,还有一个文档中未提到,但却非常有用的元件。

构造函数shape名称描述
Shape.Emptyempty无预设配置的节点。需要通过 markupattr 指定图形和样式

自定义元件

虽然官方提供了许多内置组件,但可能无法满足丰富的业务需求。这时就需要创建自定义元件了。

自定义元件的官方说法:自定义节点

如何开发一个自定义元件?

继承官方提供的节点开发元件

官方给了一个继承的示例

但除了官方文档提到的,我们甚至可以直接写继承类,不用调用静态方法。这部分的实现受到了“根据图片大小缩放节点”的启发。

这里我们以自定义开关节点示例演示直接写继承类的方式创建自定义组件

首先,绘制一个这样的开关:

image.png

SVG 图形的绘制,需要使用 markupattr 属性。其中

  • markup 负责用结构化的方式创建 SVG 中各元素的节点
  • attr 负责设置节点的样式等属性

这两部分参照代码清单中的 markupattr 部分。

const switchCenter = {
  x: 35,
  y: -2,
}
const switchOpen = `rotate(-30 ${switchCenter.x} ${switchCenter.y})`
const switchClose = `rotate(-12 ${switchCenter.x} ${switchCenter.y})`

const markup = [
  {
    tagName: 'g',
    selector: 'left-group',
    children: [
      {
        tagName: 'rect',
        selector: 'left',
        groupSelector: 'line',
        attrs: {
          x: 0,
          y: 0,
        },
      },
      {
        tagName: 'circle',
        selector: 'lco',
        groupSelector: 'co',
        attrs: {
          cx: 30,
        },
      },
      {
        tagName: 'circle',
        selector: 'lci',
        groupSelector: 'ci',
        attrs: {
          cx: 30,
        },
      },
    ],
  },
  {
    tagName: 'rect',
    selector: 'switch',
    groupSelector: 'line',
  },
  {
    tagName: 'g',
    selector: 'right-group',
    children: [
      {
        tagName: 'rect',
        selector: 'right',
        groupSelector: 'line',
        attrs: {
          x: 70,
          y: 0,
        },
      },
      {
        tagName: 'circle',
        selector: 'rco',
        groupSelector: 'co',
        attrs: {
          cx: 70,
        },
      },
      {
        tagName: 'circle',
        selector: 'rci',
        groupSelector: 'ci',
        attrs: {
          cx: 70,
        },
      },
    ],
  },
]

const attrs = {
  line: {
    width: 30,
    height: 2,
    fill: '#000',
    stroke: '#000',
  },
  co: {
    r: 8,
    fill: '#000',
  },
  ci: {
    r: 4,
    fill: '#fff',
  },
  switch: {
    ...switchCenter,
    width: 35,
    transform: switchOpen,
  },
}

上述代码中的

  • switchCenter 指定了开关的旋转中心坐标,
  • switchOpenswitchClose 分别表示开关开启和关闭时的样式。

其次,创建自定义节点

import { Graph, Shape } from '@antv/x6'

class Switch extends Shape.Empty {

  constructor(metadata) {
    super(Object.assign({}, metadata, { markup, attrs }))
  }
}

Graph.registerNode('switch', Switch, true)

注意到上面的构造函数部分了吗?

constructor(metadata) {
  super(Object.assign({}, metadata, { markup, attrs }))
}

如果有构造函数出现,必须调用 super 方法,调用时传入 metadata 值。在调用 super 方法时,可以将上面指定的样式数据传入。

然后,加上开关动态效果

在上面创建的自定义节点里面,添加 toggleOpen 方法。该方法可切换开关状态。

class Switch extends Shape.Empty {

  constructor(metadata) {
    super(Object.assign({}, metadata, { markup, attrs }))
  }

  toggleOpen() {
    const attrPath = 'attrs/switch/transform'
    const current = this.prop(attrPath)
    const target = current === switchOpen ? switchClose : switchOpen
    this.transition(attrPath, target, {
      interp: (a, b) => {
        const reg = /-?\d+/g
        const start = parseInt(a.match(reg)[0], 10)
        const end = parseInt(b.match(reg)[0], 10)
        const d = end - start
        return (t) => {
          return `rotate(${start + d * t} ${switchCenter.x} ${switchCenter.y})`
        }
      },
    })
  }
}

我们期望在点击时执行切换动作。

根据开发者的说法,需要创建一个自定义的 view,在 view 中注册绑定事件,然后在创建或声明元件时,指定为自定义的 view。

关于 view 相关信息,请查看 View

由于我们创建的是一个节点,因此对应的 view 需要继承 NodeView

具体实现过程:

首先创建一个 view
class SwitchView extends NodeView {
  onClick() {
    this.cell?.toggleOpen();
  }
}

Graph.registerView('switch', SwitchView, true)
然后修改自定义元件,指定为对应的 view

view 属性的值和 Graph.registerView 中第一个参数的值一致

class Switch extends Shape.Empty {

  constructor(metadata) {
-    super(Object.assign({}, metadata, { markup, attrs }))
+    super(Object.assign({}, metadata, { markup, attrs, view: 'switch' }))
  }
...
}

完成以上步骤,一个自定义的开关组件就完成了。

使用方式和内置组件一致:

graph.addNode({
   x: 320,
   y: 120,
   shape: 'switch'
})

完整的代码清单参照:Github Gist

使用 HTML/Vue/React 渲染内容

按照官方的说法

在 SVG 中有一个特殊的 <foreignObject> 元素,在该元素中可以内嵌任何 XHTML 元素,所以我们可以借助该元素来渲染 HTML 元素和 React 组件到需要位置。

<svg xmlns="http://www.w3.org/2000/svg">
  <foreignObject width="120" height="50">
    <body xmlns="http://www.w3.org/1999/xhtml">
      <p>Hello World</p>
    </body>
  </foreignObject>
</svg>

具体操作看文档吧:使用 HTML/React/Vue 渲染

根据文档,我们可以得到以下结论:

  • HTML 渲染内容,是通过 DOM API 进行操作
  • Vue/React 渲染内容,本质上是执行了 $mount 操作