Vue Flow 边标记(Edge Markers)实战指南:内置箭头、自定义 SVG 标记与动态交互
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,做两件事:
- 生成唯一 id:调用 getMarkerId。若标记是字符串,直接以字符串为 id;若是对象,则将其键按字典序排序后拼成
key=value&...形式,并加上vueFlowId__前缀。 - 去重:用
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/height12.5、strokeWidth1、颜色#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未填 → 回退defaultMarkerColorprop → 回退默认#b1b1b7; - id 生成规则可预测:对象式标记的 id 由排序后的键值对拼接而成(见 getMarkerId),自定义
<marker>时建议沿用边id-marker之类的命名,避免冲突; - marker 同时作用于连线:
connectionLineOptions.markerEnd/markerStart同样会被收集(见 MarkerDefinitions.vue),拖拽连线预览也可以带箭头。
至此,从内置箭头到自定义菱形/圆形/方形标记,再到"随选中状态动态切换"的交互边,你已掌握 Vue Flow 边标记的全部核心能力,可以直接套用示例代码到自己的流程图项目中。