连接桩 Port

7 min read

链接桩是节点上的固定连接点,很多图应用都有链接桩,并且有些应用还将链接桩分为输入链接桩输出连接桩

快速开始

创建节点时我们可以通过 ports 选项来配置链接桩,像下面这样:

const node = new Node({
  ports: {
    groups: { ... }, // 链接桩组定义
    items: [ ... ],  // 链接桩
  }
})

// 或者
const node = new Node({
  ports: [ ... ],  // 链接桩
})

创建链接桩

完整的配置项稍微有点复杂,后面将详细介绍。我们先基于系统的默认选项,来快速添加几个链接桩:

graph.addNode({
  x: 60,
  y: 60,
  width: 160,
  height: 80,
  label: 'Rect With Ports',
  ports: [
    { id: 'port1' }, 
    { id: 'port2' }, 
    { id: 'port3' },
  ],
})

左侧的三个圆就是我们添加的链接桩,这里使用了节点的 portMarkup 选项指定的默认的链接桩(如下)。

链接桩 Markup 可以在单个链接桩、链接桩群组和节点的 portMarkup 选项三个位置指定,优先级从高到低。

{
  tagName: 'circle',
  selector: 'circle',
  attrs: {
    r: 10,
    fill: '#fff',
    stroke: '#000',
  },
}

知道了链接桩的 DOM 结构,我们就可以来定制链接桩的样式:

graph.addNode({
  x: 60,
  y: 60,
  width: 160,
  height: 80,
  label: 'Rect With Ports',
  ports: [
    {
      id: 'port1',
      attrs: {
        circle: {
          r: 6,
          magnet: true,
          stroke: '#31d0c6',
          strokeWidth: 2,
          fill: '#fff',
        },
      },
    },
    {
      id: 'port2',
      attrs: {
        circle: {
          r: 6,
          magnet: true,
          stroke: '#31d0c6',
          strokeWidth: 2,
          fill: '#fff',
        },
      },
    },
    {
      id: 'port3',
      attrs: {
        circle: {
          r: 6,
          magnet: true,
          stroke: '#31d0c6',
          strokeWidth: 2,
          fill: '#fff',
        },
      },
    },
  ],
})

值得注意的是,我们给 circle 指定了 magnet: true 这个特殊属性,使链接桩在连线交互时可以被连接上。

上面代码中每个链接桩的样式都一样,显得有点冗长,我们可以通过 group 选项来设置链接桩分组,使该组中的链接桩具有相同的行为和样式。

看下面如何使用链接桩分组来定义链接桩样式:

graph.addNode({
  x: 60,
  y: 60,
  width: 160,
  height: 80,
  label: 'Rect With Ports',
  ports: {
    groups: {
      group1: { 
        attrs: {
          circle: {
            r: 6,
            magnet: true,
            stroke: '#31d0c6',
            strokeWidth: 2,
            fill: '#fff',
          },
        },
      },
    },
    items: [
      {
        id: 'port1',
        group: 'group1', // 指定分组名称
      },
      {
        id: 'port2',
        group: 'group1', // 指定分组名称
      },
      {
        id: 'port3',
        group: 'group1', // 指定分组名称
      },
    ],
  },
})

链接桩标签

另外,还可以为链接桩指定标签文本:

graph.addNode({
  x: 60,
  y: 60,
  width: 160,
  height: 80,
  label: 'Rect With Ports',
  ports: {
    groups: {
      group1: { 
        attrs: {
          circle: {
            r: 6,
            magnet: true,
            stroke: '#31d0c6',
            strokeWidth: 2,
            fill: '#fff',
          },
        },
      },
    },
    items: [
      {
        id: 'port1',
        group: 'group1',
        attrs: {
          text: {          // 标签选择器
            text: 'port1', // 标签文本
          },
        },
      },
      {
        id: 'port2',
        group: 'group1', 
        attrs: {
          text: {          // 标签选择器
            text: 'port2', // 标签文本
          },
        },
      },
      {
        id: 'port3',
        group: 'group1',
        attrs: {
          text: {          // 标签选择器
            text: 'port2', // 标签文本
          },
        },
      },
    ],
  },
})

这里使用了节点的 portLabelMarkup 选项指定的默认标签(如下)。

链接桩标签的 Markup 可以在单个链接桩、链接桩群组和节点的 portLabelMarkup 选项三个位置指定,优先级从高到低。

{
  tagName: 'text',
  selector: 'text',
  attrs: {
    fill: '#000000',
  },
}

连接到链接桩

有了链接桩之后,我们就可以为边指定连接的链接桩:

graph.addEdge({
  source: { x: 40, y: 100 },
  target: { 
    cell: rect, 
    port: 'port1', // 链接桩 ID
  },
})

graph.addEdge({
  source: { x: 40, y: 100 },
  target: { 
    cell: rect, 
    port: 'port2', // 链接桩 ID
  },
})

graph.addEdge({
  source: { x: 40, y: 100 },
  target: { 
    cell: rect, 
    port: 'port3', // 链接桩 ID
  },
})

上面我们快速了解了如何创建和使用链接桩,接下来我们将详细介绍一下 ports 选项中的 itemsgroups 两个配置项。

选项介绍

首先,items 是一个数组 PortMetadata[],数组的每一项表示一个链接桩,链接桩支持的选项如下:

interface PortMetadata {
  /**
   *  链接桩唯一 ID,默认自动生成。
   */ 
  id?: string   
  
  /**
   * 分组名称,指定分组后将继承分组中的链接桩选项。
   */
  group?: string
  
  /**
   * 为群组中指定的链接桩布局算法提供参数。
   * 我们不能为单个链接桩指定布局算法,但可以为群组中指定的布局算法提供不同的参数。
   */
  args?: object

  /**
   * 链接桩的 DOM 结构定义。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。
   */
  markup?: Markup
  
  /**
   * 元素的属性样式。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。
   */
  attrs?: Attr.CellAttrs
  
  /**
   * 链接桩的 DOM 层级,值越大层级越高。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。
   */
  zIndex?: number | 'auto'

  /**
   * 链接桩的标签。指定该选项后将覆盖 `group` 指代的群组提供的默认选项。
   */
  label?: {
    markup?: Markup // 标签 DOM 结构
    position?: {    // 标签位置
      name: string  // 标签位置计算方法的名称
      args?: object // 标签位置计算方法的参数
    }
  }
}

然后,通常我们将具有相同行为和外观的链接桩归为同一组,并通过 groups 选项来设置分组,该选项是一个对象 { [groupName: string]: PortGroupMetadata },组名为键,值为每组链接桩的默认选项,支持的选项如下:

interface PortGroupMetadata {
  /**
   * 链接桩 DOM 结构定义。
   */
  markup?: Markup
  
  /**
   * 属性和样式。
   */
  attrs?: Attr.CellAttrs
  
  /**
   * 链接桩的 DOM 层级,值越大层级越高。
   */
  zIndex?: number | 'auto'

  /**
   * 群组中链接桩的布局。
   */
  position?: 
    | [number, number] // 绝对定位
    | string           // 链接桩布局方法的名称
    | {                // 链接桩布局方法的名称和参数
        name: string
        args?: object
      }
  
  /**
   * 链接桩标签。
   */
  label?: {
    markup?: Markup
    position?: {    // 链接桩标签布局
      name: string  // 布局名称
      args?: object // 布局参数
    }
  }
}

例如:

const node = new Node({
  ports: {
    group: { 
      group1: { 
        markup: {
          tagName: 'circle',
          selector: 'circle',
          attrs: {
            r: 10,
            fill: '#fff',
            stroke: '#000',
          },
        },
        attrs: { 
          circle: {
            r: 6,
          },
        },
        zIndex: 1,
        position: {
          name: 'top',
          args: {},
        },
        label: { 
          markup: {
            tagName: 'text',
            selector: 'text',
            attrs: {
              fill: '#000',
            },
          },
        },
      },
      group2: { ... },
      group3: { ... },
    },
    items: [
      { id: 'port1', group: 'group1', ... },
      { id: 'port2', group: 'group1', ... },
      { id: 'port3', group: 'group2', ... },
    ],
  }
})

连接桩和标签位置

上面我们使用默认位置创建了几个连接桩,并为链接桩设置了标签,下面我们来看看如何为链接桩和标签指定位置。

链接桩位置

链接桩布局算法只能通过 groups 中的 position 选项来指定,因为布局算法在计算链接桩位置是需要考虑到群组中的所有连接桩,我们在单个链接桩中可以通过 args 选项来影响该链接桩的布局结果。

我们默认提供了下面几种链接桩布局算法,同时支持自定义链接桩布局算法并注册使用,点击下面的链接可以了解每种布局算法的使用方法。

下面我们就来定制一个具有输入和输出链接桩的节点。

graph.addNode({
  x: 60,
  y: 60,
  width: 180,
  height: 60,
  label: 'In Ports & Out Ports',
  ports: {
    groups: {
      // 输入链接桩群组定义
      in: {
        position: 'top',
        attrs: {
          circle: {
            r: 6,
            magnet: true,
            stroke: '#31d0c6',
            strokeWidth: 2,
            fill: '#fff',
          },
        },
      },
      // 输出链接桩群组定义
      out: {
        position: 'bottom',
        attrs: {
          circle: {
            r: 6,
            magnet: true,
            stroke: '#31d0c6',
            strokeWidth: 2,
            fill: '#fff',
          },
        },
      },
    },
    items: [
      {
        id: 'port1',
        group: 'in',
      },
      {
        id: 'port2',
        group: 'in',
      },
      {
        id: 'port3',
        group: 'in',
      },
      {
        id: 'port4',
        group: 'out',
      },
      {
        id: 'port5',
        group: 'out',
      },
    ],
  },
})

标签位置

groupslabel.position 选项和节点的 label.position 选项中都可以指定标签的位置。

我们默认提供了下面几种标签位置,也支持自定义标签位置并注册使用,点击下面的链接了解每种标签位置的使用方法。

  • left 标签位于链接桩左侧。
  • right 标签位于链接桩右侧。
  • top 标签位于链接桩上方。
  • bottom 标签位于链接桩下方。
  • inside 标签位于节点内围(靠近边线的内侧)。
  • outside 标签位于节点外围(靠近边线的外侧)。
  • insideOriented 标签位于节点内围,而且根据所在方位自动调整文本的方向。
  • outsideOriented 标签位于节点外围,而且根据所在方位自动调整文本的方向。
  • radial 标签位于圆形或椭圆形节点的外围。
  • radialOriented 标签位于圆形或椭圆形节点的外围,并使标签文本自动沿圆弧方向旋转。

下面我们为刚刚创建的输入、输出连接桩指定一下标签和标签位置。

graph.addNode({
  x: 60,
  y: 50,
  width: 180,
  height: 80,
  label: 'In Ports & Out Ports',
  ports: {
    groups: {
      in: {
        position: 'top',    // 链接桩位置
        label: {
          position: 'top',  // 标签位置
        },
        attrs: {
          circle: {
            r: 6,
            magnet: true,
            stroke: '#31d0c6',
            strokeWidth: 2,
            fill: '#fff',
          },
        },
      },
      out: {
        position: 'bottom',   // 链接桩位置
        label: {
          position: 'bottom', // 标签位置
        },
        attrs: {
          circle: {
            r: 6,
            magnet: true,
            stroke: '#31d0c6',
            strokeWidth: 2,
            fill: '#fff',
          },
        },
      },
    },
    items: [
      {
        id: 'port1',
        group: 'in',
        attrs: {
          text: { text: 'in1' },
        },
      },
      {
        id: 'port2',
        group: 'in',
        attrs: {
          text: { text: 'in2' },
        },
      },
      {
        id: 'port3',
        group: 'in',
        attrs: {
          text: { text: 'in3' },
        },
      },
      {
        id: 'port4',
        group: 'out',
        attrs: {
          text: { text: 'out1' },
        },
      },
      {
        id: 'port5',
        group: 'out',
        attrs: {
          text: { text: 'out2' },
        },
      },
    ],
  },
})

最佳实践

链接桩选项多、配置代码长,推荐的做法是,基于群组将链接桩的通用选项定义为节点的默认选项。例如我们可以定义一个矩形节点,然后为该矩形节点设置预定义的输入和输出链接桩。

Shape.Rect.define({
  shape: 'my-rect',
  width: 180,
  height: 80,
  ports: {
    groups: {
      in: {
        position: 'top',
        label: {
          position: 'top',
        },
        attrs: {
          circle: {
            r: 6,
            magnet: true,
            stroke: '#31d0c6',
            strokeWidth: 2,
            fill: '#fff',
          },
        },
      },
      out: {
        position: 'bottom',
        label: {
          position: 'bottom',
        },
        attrs: {
          circle: {
            r: 6,
            magnet: true,
            stroke: '#31d0c6',
            strokeWidth: 2,
            fill: '#fff',
          },
        },
      },
    },
  },
})

上面我们定义并注册了名为 my-rect 的矩形,并为该矩形指定了输入和输出桩,我们可以这样来使用:

graph.addNode({
  x: 60,
  y: 50,
  shape: 'my-rect',
  label: 'In Ports & Out Ports',
  ports: [
    {
      id: 'port1',
      group: 'in',
    },
    {
      id: 'port2',
      group: 'in',
    },
    {
      id: 'port3',
      group: 'in',
    },
    {
      id: 'port4',
      group: 'out',
    },
    {
      id: 'port5',
      group: 'out',
    },
  ],
})

使用 React 渲染连接桩

除了上面的方式,还可以用 React 组件来渲染连接桩,先看效果:

详细使用见渲染链接桩