Vue Flow 边标记(Edge Markers)实战指南:内置箭头、自定义 SVG 标记与动态交互

原创2026-10-03 10:36:521,754 阅读
文章标签:前端UI组件图表库

Vue Flow 边标记(Edge Markers)实战指南:内置箭头、自定义 SVG 标记与动态交互

本篇指南以 Vue Flow 官方示例 docs/examples/edge-markers(对应文档 docs/src/examples/edges/markers.md)为骨架,系统讲解边标记 markerStart / markerEnd 的完整用法:从内置 MarkerType 枚举与 EdgeMarker 配置对象,到标记的自动去重注入机制,再到如何用 SVG <marker> 定义菱形、圆形、方形等自定义标记,并实现"随源/目标节点选中状态动态变色变型"的交互效果。读完你既能直接照抄示例跑通,也能理解 Vue Flow 底层 MarkerDefinitions.vue 如何为你生成并管理 marker 定义。

一、边标记是什么

在流程图中,边的两端(起点与终点)常需要箭头或形状符号来指示方向与语义。Vue Flow 通过边对象上的两个可选属性控制这一能力:

  • markerStart:渲染在边的起点(源端)的标记;
  • markerEnd:渲染在边的终点(目标端)的标记。

两者的类型均为 EdgeMarkerType,在 packages/core/src/types/edge.ts 中定义为 string | MarkerType | EdgeMarker,即有三种声明方式:枚举值字符串、MarkerType 枚举、EdgeMarker 配置对象。声明后,你无需手动书写任何 SVG,Vue Flow 会在渲染边之前自动收集所有标记并注入到图层的 <defs> 中。

二、内置标记类型与配置项

Vue Flow 内置两类箭头,定义在 MarkerType 枚举中(见 packages/core/src/types/edge.ts):

枚举值 值 外观
MarkerType.Arrow 'arrow' 空心(开放式)箭头,仅描边不填充
MarkerType.ArrowClosed 'arrowclosed' 实心(闭合式)箭头,描边并填充

内置箭头符号的实际 SVG 形状定义在 packages/core/src/container/EdgeRenderer/MarkerSymbols.vue 中:Arrow 是一个 fill="none" 的三点 polyline(-5,-4 0,0 -5,4),ArrowClosed 则是带填充的闭合 polyline。

如果枚举值不足以满足需求,可以用 EdgeMarker 对象精细定制,其完整字段(见 edge.ts):

字段 类型 说明
id string 标记唯一 id;缺省时由内部根据属性自动生成
type MarkerType 标记类型(必填)
color string 标记颜色;缺省时回退到全局 defaultMarkerColor
width / height number 标记渲染尺寸,默认 12.5
markerUnits string 标记单位,默认 'strokeWidth'
orient string 标记方向,默认 'auto-start-reverse'
strokeWidth number 描边宽度,默认 1

全局默认颜色 defaultMarkerColor 的值为 '#b1b1b7'(灰),定义在 packages/core/src/store/state.ts,可通过 VueFlow 组件的同名 prop 覆盖。

三、底层机制:标记如何被收集与注入

当你给边声明 markerStart / markerEnd 后,无需手动添加 <marker> 定义,这是 packages/core/src/container/EdgeRenderer/MarkerDefinitions.vue 的功劳。它通过 useVueFlow() 拿到当前 edges 与 connectionLineOptions,遍历其中所有 markerStart / markerEnd,做两件事:

  1. 生成唯一 id:调用 getMarkerId。若标记是字符串,直接以字符串为 id;若是对象,则将其键按字典序排序后拼成 key=value&... 形式,并加上 vueFlowId__ 前缀。
  2. 去重:用 Set 记录已生成的 id,相同配置的标记只注入一次(例如多条边共用同一个箭头时,<defs> 中只有一份定义)。

随后 MarkerSymbols.vue 负责把每个标记渲染为真正的 <marker> 元素,包含 viewBox="-10 -10 20 20"、refX="0"、refY="0"、markerUnits、orient 等属性。边组件侧则通过 url(#markerId) 引用这些标记,引用逻辑见 EdgeWrapper.ts。了解这一点后,你就能理解自定义标记为何也必须保证 id 唯一且可被 url(#...) 引用。

四、三种声明方式实战

以示例 docs/examples/edge-markers/App.vue 为蓝本,定义 6 个节点和 3 条边,分别演示三种写法:

<script setup>
import { ref } from 'vue'
import { MarkerType, VueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import CustomEdge from './CustomEdge.vue'

const nodes = ref([
  { id: '1', position: { x: 0, y: 0 }, data: { label: 'Select me for diamond markers' } },
  { id: '2', position: { x: 0, y: 150 }, data: { label: 'Select me for circle markers' } },
  { id: '3', position: { x: 200, y: 0 }, data: { label: 'Node 3' } },
  { id: '4', position: { x: 200, y: 150 }, data: { label: 'Node 4' } },
  { id: '5', position: { x: 400, y: 0 }, data: { label: 'Node 5' } },
  { id: '6', position: { x: 400, y: 150 }, data: { label: 'Node 6' } },
])

const edges = ref([
  // 方式一:使用自定义边类型,标记由 CustomEdge.vue 内部决定
  { id: '1-2', source: '1', target: '2', type: 'custom' },
  // 方式二:直接使用 MarkerType 枚举
  {
    id: '3-4',
    source: '3',
    target: '4',
    label: 'Marker Arrow',
    markerEnd: MarkerType.Arrow,
    markerStart: MarkerType.Arrow,
  },
  // 方式三:使用 EdgeMarker 对象定制颜色
  {
    id: '5-6',
    source: '5',
    target: '6',
    label: 'Marker ArrowClosed',
    markerEnd: {
      type: MarkerType.ArrowClosed,
      color: '#ff0072',
    },
    markerStart: {
      type: MarkerType.ArrowClosed,
      color: '#ff0072',
    },
  },
])
</script>

<template>
  <VueFlow :nodes="nodes" :edges="edges" fit-view-on-init>
    <template #edge-custom="edgeProps">
      <CustomEdge v-bind="edgeProps" />
    </template>
    <Background />
  </VueFlow>
</template>

要点:

  • 枚举写法最简洁,两端都指向同一个箭头类型,颜色使用全局默认灰;
  • 对象写法可单独指定 color 等样式,示例中 #ff0072(洋红)让两端箭头更醒目;
  • 自定义边写法(type: 'custom')配合 <template #edge-custom> 插槽注册组件,标记完全由组件内部渲染,适合实现动态逻辑——这正是下一节的内容。

模板中的 <Background /> 来自 @vue-flow/background,示例其余部分可参考 style.css(该示例隐藏了 handle 并提升边层级,便于观察标记效果)。

五、自定义 SVG 标记:菱形、圆形与方形

内置箭头只有两种,若要菱形(diamond)、圆形(circle)、方形(square)等符号,需要自己用 SVG <marker> 定义。示例 CustomMarker.vue 给出一个可直接复用的通用实现:

<script setup>
defineProps({
  id: { type: String, required: true },
  type: { type: String, required: true },
  stroke: { type: String, required: false, default: '#b1b1b7' },
  fill: { type: String, required: false, default: '#b1b1b7' },
  strokeWidth: { type: Number, required: false, default: 1 },
  width: { type: Number, required: false, default: 12.5 },
  height: { type: Number, required: false, default: 12.5 },
})
</script>

<template>
  <svg class="vue-flow__marker vue-flow__container">
    <defs>
      <marker
        :id="id"
        class="vue-flow__arrowhead"
        viewBox="-10 -10 20 20"
        refX="0"
        refY="0"
        :markerWidth="width"
        :markerHeight="height"
        markerUnits="strokeWidth"
        orient="auto-start-reverse"
      >
        <path
          v-if="type === 'diamond'"
          :style="{ stroke, strokeWidth }"
          stroke-linecap="round"
          stroke-linejoin="round"
          :fill="fill"
          d="M 0,-5 L 5,0 L 0,5 L -5,0 Z"
        />
        <path
          v-if="type === 'circle'"
          :style="{ stroke, strokeWidth }"
          stroke-linecap="round"
          stroke-linejoin="round"
          :fill="fill"
          d="M 0,-4 a 4,4 0 1,0 0,8 a 4,4 0 1,0 0,-8"
        />
        <path
          v-if="type === 'square'"
          :style="{ stroke, strokeWidth }"
          stroke-linecap="round"
          stroke-linejoin="round"
          :fill="fill"
          d="M -4 -4 H 4 V 4 H -4 Z"
        />
      </marker>
    </defs>
  </svg>
</template>

关键设计:

  • id 必须唯一:边通过 url(#markerId) 引用它(见下一节),id 冲突会导致所有边指向同一个标记;
  • 坐标系:viewBox="-10 -10 20 20" 把原点放在中心,refX="0"、refY="0" 使标记尖端精确落在边的端点,配合 orient="auto-start-reverse" 实现起点、终点自动反向;
  • 三个 path 共享同一坐标系:菱形用 Z 闭合路径,圆形用两段弧形 a 4,4 ...,方形用 H/V 折线,均支持独立设置 stroke(描边色)、fill(填充色)与 strokeWidth;
  • 默认值与 Vue Flow 内置标记保持一致(width/height 12.5、strokeWidth 1、颜色 #b1b1b7),可无缝混用。

六、动态标记:随节点选中状态实时切换

自定义标记的真正价值在于动态响应。示例 CustomEdge.vue 演示了完整实现:边的两端标记会根据源/目标节点是否被选中,实时切换形状与颜色。

<script setup>
import { computed } from 'vue'
import { BaseEdge, getBezierPath, useVueFlow } from '@vue-flow/core'
import CustomMarker from './CustomMarker.vue'

const props = defineProps({
  id: { type: String, required: true },
  sourceX: { type: Number, required: true },
  sourceY: { type: Number, required: true },
  targetX: { type: Number, required: true },
  targetY: { type: Number, required: true },
  sourcePosition: { type: String, required: true },
  targetPosition: { type: String, required: true },
  source: { type: String, required: true },
  target: { type: String, required: true },
  data: { type: Object, required: false },
})

const { findNode } = useVueFlow()

// 计算贝塞尔路径,path[0] 为 d 路径,path[1]/path[2] 为标签坐标
const path = computed(() => getBezierPath(props))

// 标记 id 与边 id 绑定,保证唯一
const markerId = computed(() => `${props.id}-marker`)

const markerColor = computed(() => {
  const sourceNode = findNode(props.source)
  const targetNode = findNode(props.target)
  if (sourceNode.selected) return '#ff0072'   // 源节点选中:洋红
  if (targetNode.selected) return '#2563eb'   // 目标节点选中:蓝
  return '#4a5568'                            // 默认:深灰
})

const markerType = computed(() => {
  const sourceNode = findNode(props.source)
  const targetNode = findNode(props.target)
  if (sourceNode.selected) return 'diamond'
  if (targetNode.selected) return 'circle'
  return 'square'
})
</script>

<script>
export default {
  inheritAttrs: false,
}
</script>

<template>
  <BaseEdge
    :id="id"
    :path="path[0]"
    :marker-end="`url(#${markerId})`"
    :marker-start="`url(#${markerId})`"
    :label="`${markerType} marker`"
    :label-x="path[1]"
    :label-y="path[2]"
    label-bg-style="fill: whitesmoke"
  />
  <CustomMarker :id="markerId" :type="markerType" :stroke="markerColor" :stroke-width="2" :width="20" :height="20" />
</template>

实现要点拆解:

  • 路径计算:getBezierPath(props) 接收 sourceX/sourceY/targetX/targetY/sourcePosition/targetPosition 等标准边 props,返回 [d 路径, 标签x, 标签y],这是自定义边的通用做法;
  • 状态读取:useVueFlow() 提供的 findNode(id) 返回 GraphNode,其上带有 selected 响应式属性,因此 computed 能自动追踪选中变化并触发重渲染;
  • 标记渲染:BaseEdge 只负责画线与引用标记(marker-start/marker-end 指向 url(#id)),真正的 <marker> 定义由紧随其后的 <CustomMarker> 输出。两端共用一个 markerId,但依赖 orient="auto-start-reverse" 自动区分起点/终点方向;
  • 标签联动:边标签直接显示当前标记类型文本(diamond marker / circle marker / square marker),配合 label-bg-style="fill: whitesmoke" 提升可读性;
  • inheritAttrs: false:防止多余 DOM 属性透传到 BaseEdge 之外,保持组件行为可控。

七、运行示例与进阶提示

在本地运行该示例(仓库采用 pnpm workspace 管理,相关配置见 pnpm-workspace.yaml 与 package.json):

pnpm install        # 安装依赖
pnpm dev            # 启动开发服务器,按提示打开对应端口

进阶要点回顾:

  • 去重是自动的:多条边声明相同标记时,MarkerDefinitions.vue 只注入一份定义,无需手工管理 <defs>;
  • 颜色回退链路:EdgeMarker.color 未填 → 回退 defaultMarkerColor prop → 回退默认 #b1b1b7;
  • id 生成规则可预测:对象式标记的 id 由排序后的键值对拼接而成(见 getMarkerId),自定义 <marker> 时建议沿用 边id-marker 之类的命名,避免冲突;
  • marker 同时作用于连线:connectionLineOptions.markerEnd/markerStart 同样会被收集(见 MarkerDefinitions.vue),拖拽连线预览也可以带箭头。

至此,从内置箭头到自定义菱形/圆形/方形标记,再到"随选中状态动态切换"的交互边,你已掌握 Vue Flow 边标记的全部核心能力,可以直接套用示例代码到自己的流程图项目中。

登录后查看全文
vue-flow