LogicFlow edgeModel 深入解析:数据属性、样式钩子与自定义边模型实战
【免费下载链接】LogicFlowA flow chart editing framework focus on business customization. 专注于业务自定义的流程图编辑框架,支持实现脑图、ER图、UML、工作流等各种图编辑场景。项目地址: https://gitcode.com/GitHub_Trending/lo/LogicFlow
每一条连线(edge)在 LogicFlow 中都会对应一个独立的edgeModel,它是连接数据与渲染的枢纽:数据驱动渲染,状态变更则应通过框架提供的 API 完成。本文以官方 API 文档 edgeModel.en.md 为核心骨架,结合@logicflow/core源码,系统讲解 edgeModel 的数据属性、形状属性、样式钩子、生命周期方法,以及如何通过自定义边模型(Custom Edge Model)实现折线、贝塞尔曲线、动画、动态偏移量等业务场景。读完本文,你将能够直接上手注册自定义边,并利用主题管道(theme pipeline)精确控制连线的外观与交互。
一、核心心智模型:数据驱动渲染,API 驱动状态
edgeModel 的设计遵循与 nodeModel 一致的模式:数据(EdgeConfig)负责驱动渲染,状态变更必须走内置的公开 API。官方文档在开头给出了一个醒目的警告:
Direct field writes can desynchronize anchors, bend points, and text overlays. Use built-in helpers whenever possible. (直接修改字段会导致锚点、拐点与文本浮层失步,请尽可能使用内置 API。)
这一点在源码中体现得淋漓尽致。BaseEdgeModel的构造函数(BaseEdgeModel.ts)只做了两件事:
constructor(data: EdgeConfig<P>, graphModel: GraphModel) { this.graphModel = graphModel this.properties = data.properties ?? ({} as P) this.initEdgeData(data) this.setAttributes() }而initEdgeData(BaseEdgeModel.ts)内部存在严格的初始化顺序依赖:
initEdgeData(data: EdgeConfig) { // 1. 补齐 properties 与 id(自定义 id > 全局 idGenerator > 内置 uuid) // 2. 绑定箭头 marker 引用 url(#marker-start-xxx) / url(#marker-end-xxx) this.setAnchors() // 端点依赖 sourceNode / targetNode this.initPoints() // 拐点依赖两个端点 this.formatText(data) // 文本位置依赖边上的所有拐点 }注释里明确写明了依赖链:端点依赖源/目标节点,拐点依赖端点,文本位置依赖拐点。这正是文档警告背后真正的原因——如果绕过 API 直接改字段,这一串级联计算(setAnchors→initPoints→formatText)就不会被触发,锚点、折点、文本三者随即失步。
二、数据属性(Data properties)
边的数据载荷由EdgeConfig字段与运行期缓存组合而成。EdgeConfig的完整定义位于 LogicFlow.tsx:
export interface EdgeConfig<P extends PropertiesType = PropertiesType> { id?: string type?: string // LogicFlow 内部默认 polyline sourceNodeId: string // 源节点 id(必填) sourceAnchorId?: string // 源锚点 id targetNodeId: string // 目标节点 id(必填) targetAnchorId?: string // 目标锚点 id startPoint?: Point // 手动指定的起点 endPoint?: Point // 手动指定的终点 text?: TextConfig | string // 边文本(字符串或对象) pointsList?: Point[] // 折线拐点 zIndex?: number // 层级 properties?: P // 业务自定义属性 }各字段说明如下:
| 字段 | 类型 | 说明 |
|---|---|---|
id | string(可选) | 边标识,缺省时按"自定义 id > 全局idGenerator> 内置 uuid"三级策略生成 |
type | string(可选) | 边类型,默认polyline |
sourceNodeId | string | 源节点 id,必填 |
sourceAnchorId | string(可选) | 源锚点 id |
targetNodeId | string | 目标节点 id,必填 |
targetAnchorId | string(可选) | 目标锚点 id |
startPoint | Point(可选) | 手动起点坐标 |
endPoint | Point(可选) | 手动终点坐标 |
text | string | TextConfig(可选) | 文本标签 |
pointsList | Point[](可选) | 折线拐点序列 |
zIndex | number(可选) | 层级,受overlapMode影响 |
properties | PropertiesType(可选) | 自定义业务属性,类型见 MainTypes.en.md |
与EdgeConfig相对的运行期形态是EdgeData(LogicFlow.tsx),它是在边存活期间由模型维护的缓存快照,id、type、startPoint、endPoint已被解析为确定值,getData()返回的就是它:
export interface EdgeData extends EdgeConfig { id: string type: string text?: TextConfig startPoint: Point endPoint: Point [key: string]: unknown }在BaseEdgeModel中,这些数据字段全部声明为 MobX 可观察属性(BaseEdgeModel.ts):
@observable type = '' @observable sourceNodeId = '' @observable targetNodeId = '' @observable startPoint!: Point @observable endPoint!: Point @observable text: Required<TextConfig> = { value: '', x: 0, y: 0, draggable: false, editable: true } @observable properties: P @observable points = '' // 渲染用路径串,如 "100,100 200,200" @observable pointsList: Point[] = [] // 结构化拐点数组值得注意:points是渲染层使用的 SVG path 字符串,pointsList是结构化的点数组,二者由各类边模型的getPath()/initPoints()维持同步。
端点的解析:getBeginAnchor 与 getEndAnchor
当EdgeConfig未显式给出sourceAnchorId/targetAnchorId或起终点时,setAnchors()(BaseEdgeModel.ts)会调用getBeginAnchor/getEndAnchor自动计算:
- 若指定了锚点 id 且该锚点存在于节点上,则直接使用该锚点坐标;
- 否则遍历节点全部锚点,选择与目标节点(或起点)欧氏距离最近的锚点作为连接点;
- 若锚点列表为空(如自定义
getDefaultAnchor()返回[]),会抛出明确错误:"无法获取 beginAnchor,请检查 anchors 相关逻辑,anchors 不能为空"。
三、形状属性(Shape attributes)
官方文档指出:影响路径走向的属性——控制点、端点、可调整线段——都应放在setAttributes()中维护,以保证依赖它的计算始终一致。
从源码结构看,LogicFlow 内置了三种基础边模型(model/edge/index.ts),它们的形状计算策略各不相同:
1. LineEdgeModel:直线
LineEdgeModel.ts 中路径与文本位置都直接由两个端点推导:
getPath(points: Point[]): string { const [start, end] = points return `${start.x},${start.y} ${end.x},${end.y}` } getTextPosition(): Point { return { x: (this.startPoint.x + this.endPoint.x) / 2, y: (this.startPoint.y + this.endPoint.y) / 2 } }文本默认居中于线段中点。
2. PolylineEdgeModel:折线(默认边型)
折线是最复杂的边型,其核心是orthogonalizePath(正交化)与updatePoints两个方法:
initEdgeData(PolylineEdgeModel.ts):若配置中提供了pointsList,会先做正交化校正,同时计算默认offset(箭头与折线重叠长度 + 5);updatePoints(PolylineEdgeModel.ts):调用工具函数getPolylinePoints,结合起终点、源/目标节点包围盒与offset重新计算折点;- 拖拽拐点时,
dragAppend/dragAppendSimple会先调整线段端点坐标,再通过removeCrossPoints/getDraggingPoints/updateCrossPoints等算法把线段吸附到节点外框、圆角矩形、圆形、椭圆、菱形等多边形边界上(见 PolylineEdgeModel.ts)。
offset是折线的关键形状属性——它控制边从锚点伸出、垂直/水平拐弯的"避让距离",且支持通过properties.offset动态设置(setAttributes中检测到变化会触发updatePoints)。
3. BezierEdgeModel:贝塞尔曲线
BezierEdgeModel.ts 中,形状由起终点加上两个控制点决定:
private getControls(): IBezierControls { const start = this.startPoint const end = this.endPoint return getBezierControlPoints({ start, end, sourceNode: this.sourceNode, targetNode: this.targetNode, offset: this.offset }) } getPath(points: Point[]): string { const [start, sNext, ePre, end] = points return `M ${start.x} ${start.y} C ${sNext.x} ${sNext.y}, ${ePre.x} ${ePre.y}, ${end.x} ${end.y}` }贝塞尔的offset默认值为100(BezierEdgeModel.ts),控制点随端点移动同步平移(moveStartPoint/moveEndPoint),并可通过updateAdjustAnchor(anchor, 'sNext' | 'ePre')单独调整。
从源码结构看,若要实现"动态偏移量"这类形状需求,正确姿势是重写
setAttributes(),在properties变化时更新形状相关字段——官方示例 basicEdge.tsx 中即注册了dynamicOffsetPolyline与dynamicOffsetBezier两个演示模型。
四、样式属性(Style attributes)与主题管道
文档强调:自定义边的主题定制应通过重写样式钩子接入 LogicFlow 的主题管道(theme pipeline),而不是手工修改 SVG 片段。BaseEdgeModel提供了五个核心钩子(全部标注@overridable,即支持重写):
| 钩子方法 | 作用 | 默认实现来源 |
|---|---|---|
getEdgeStyle() | 边主体样式 | theme.baseEdge+this.style,见 BaseEdgeModel.ts |
getTextStyle() | 边文本样式 | theme.edgeText深拷贝 |
getArrowStyle() | 箭头样式 | 由getEdgeStyle()、getEdgeAnimationStyle()与theme.arrow合成,见 BaseEdgeModel.ts |
getOutlineStyle() | 选中/悬停轮廓样式 | theme.edgeOutline(悬停时叠加hover子项) |
getEdgeAnimationStyle() | 边动画样式 | theme.edgeAnimation深拷贝 |
getAdjustPointStyle() | 端点调整点样式 | theme.edgeAdjust |
这些钩子返回的样式对象最终由渲染层消费,从而保证"样式一致性与主题系统联动"。核心主题默认值定义在 theme.ts:
baseEdge: { stroke: '#474747', strokeWidth: 2, radius: 12 }, edgeText: { textWidth: 100, overflowMode: 'default', fontSize: 12, background: { fill: '#fff' } }, arrow: { strokeLinecap: 'round', strokeLinejoin: 'round', offset: 10, verticalLength: 5 }, edgeOutline: { fill: 'transparent', stroke: '#757575', strokeWidth: 1.5, strokeDasharray: '4,4', radius: 8, hover: { stroke: '#4271DF' } }, edgeAnimation: { stroke: '#4271DF', strokeDasharray: '12,4,6,4', strokeDashoffset: '100%', animationName: 'lf_animate_dash', animationDuration: '20s', animationIterationCount: 'infinite', animationTimingFunction: 'linear', animationDirection: 'normal', },各派生边模型会在getEdgeStyle()中叠加各自的类型主题:直线叠加theme.line、折线叠加theme.polyline、贝塞尔叠加theme.bezier,并且都支持通过properties.style传自定义样式(优先级最高):
getEdgeStyle() { const { polyline } = this.graphModel.theme const style = super.getEdgeStyle() const { style: customStyle = {} } = this.properties return { ...style, ...cloneDeep(polyline), ...cloneDeep(customStyle) } }动画控制
动画由状态字段isAnimation驱动,并配有两个公开 API(BaseEdgeModel.ts):
@action openEdgeAnimation(): void { this.isAnimation = true } @action closeEdgeAnimation(): void { this.isAnimation = false }isAnimation还会参与箭头样式的合成(动画开启时箭头使用动画描边色)。
五、生命周期方法与状态 API
官方文档建议:initEdgeData、setAttributes、历史快照等更多生命周期方法,可查阅@logicflow/core随包发布的BaseEdgeModel类型定义。以下是源码中确认的核心 API 一览:
初始化与更新
initEdgeData(data):初始化边数据,仅构造时调用一次,用于初始化所有属性(含 id 生成、锚点、路径、文本);setAttributes():空实现,每次properties变化都会重新触发,是响应式更新形状/样式的主钩子;getData():返回序列化数据(含pointsList、文本、zIndex,历史记录与保存用);getHistoryData():历史快照数据源,可通过重写决定哪些属性变化不进入 history。
properties 变更(响应式核心)
@action setProperty(key, val) // 设置单个属性,自动触发 setAttributes @action deleteProperty(key) // 删除单个属性 @action setProperties(properties) // 合并更新多个属性,触发重新渲染 getProperties(): P // 返回 toJS 后的 properties 快照这三组 API 是"通过 API 改状态"的最佳示范——它们都会在修改后调用setAttributes(),让形状与样式钩子重新求值,从而避免字段直写导致的失步问题。
文本 API
| 方法 | 作用 |
|---|---|
setText(config) | 设置文本位置与值(TextConfig) |
updateText(value) | 仅更新文本内容 |
moveText(dx, dy) | 按增量移动文本 |
resetTextPosition() | 将文本位置重置到getTextPosition()计算结果 |
getTextPosition() | 返回文本应在的位置(各边型重写实现不同) |
setTextMode(mode) | 设置文本模式 |
其中formatText(BaseEdgeModel.ts)在初始化时根据editConfigModel.edgeTextDraggable/edgeTextEdit等全局配置为文本设置默认的draggable/editable值,并支持TextConfig对象覆盖。
端点与路径
updateStartPoint(anchor) / updateEndPoint(anchor) // 更新端点(折线/贝塞尔会级联重算路径) moveStartPoint(dx, dy) / moveEndPoint(dx, dy) // 增量移动端点 updatePointsList(dx, dy) // 整体平移拐点 updateAttributes(attributes) // 批量赋值(谨慎使用) setZIndex(zIndex)折线与贝塞尔模型都重写了updateStartPoint/updateEndPoint:移动端点后会自动调用updatePoints()重算整条路径,这也是"依赖计算保持连贯"的设计体现。
状态与交互
setSelected(flag) / setHovered(flag) // 选中 / 悬停状态 setHitable(flag) / setHittable(flag) // 是否响应命中(细粒度交互控制) openEdgeAnimation() / closeEdgeAnimation() setElementState(state, additionStateData) // 元素状态(编辑、菜单等) getAdjustStart() / getAdjustEnd() // 端点调整的锚点 updateAfterAdjustStartAndEnd({startPoint, endPoint, ...}) // 起终点拖拽调整后的路径更新此外还有计算属性sourceNode/targetNode(通过graphModel.nodesMap映射到源/目标节点模型),以及changeEdgeId(id)(同步更新箭头 marker 引用)。
六、实战:注册一条自定义边模型
官方示例 polyline.tsx 完整演示了"模型 + 视图 + 注册"的完整链路。核心分为三步。
第一步:重写 Model 接入主题管道
class CustomPolylineModel extends PolylineEdgeModel { initEdgeData(data: LogicFlow.EdgeConfig) { super.initEdgeData(data); this.customTextPosition = true; // 使用自定义文本位置 } // 依据 properties.textPosition 动态返回文本位置 getTextPosition(): LogicFlow.Point { /* ...计算 start / center / end... */ } // 依据 properties.openAnimation 控制动画 setAttributes() { const { openAnimation } = this.properties; this.isAnimation = !!openAnimation; } // 重写动画样式钩子 getEdgeAnimationStyle() { const style = super.getEdgeAnimationStyle(); style.strokeDasharray = '15 5'; style.animationDuration = '10s'; style.stroke = 'rgb(130, 179, 102)'; return style; } // 依据 properties.edgeWeight / highlight 控制粗细与颜色 getEdgeStyle() { const style = super.getEdgeStyle(); const { edgeWeight, highlight } = this.properties; style.strokeWidth = edgeWeight ? 5 : 3; style.stroke = highlight ? 'red' : 'black'; return style; } }这里可以看到三个钩子的配合:setAttributes()负责把properties映射为状态字段,getEdgeStyle()/getEdgeAnimationStyle()负责把状态映射为主题样式,职责清晰、互不污染。
第二步:重写 View 定制箭头
箭头形状属于渲染层,可在自定义 View 中重写getEndArrow/getStartArrow,返回任意 SVG 图形(示例用h()创建 path),并通过model.getArrowStyle()获取描边色与线宽,实现空心箭头、半箭头、实心箭头三种形态的切换。
第三步:注册并设为默认边型
const CustomPolylineEdge = { type: 'customPolyline', // 边类型名称 model: CustomPolylineModel, // 数据模型 view: CustomPolyline, // 渲染视图 }; const lf = new LogicFlow({ ...config, container, grid: { size: 10 } }); lf.register(CustomPolylineEdge); lf.setDefaultEdgeType('customPolyline'); // 全局默认连线类型 lf.render({ nodes: [ /* rect 节点 */ ], edges: [ { id: 'edge-1', sourceNodeId: '1', targetNodeId: '2', type: 'customPolyline', properties: { edgeWeight: true, textPosition: 'center' }, }, ], });注册后即可在render数据中通过type: 'customPolyline'使用;也可以通过实例级edgeType配置(MainTypes.en.md)或edgeGenerator函数(MainTypes.en.md)按源/目标节点类型动态决定连线类型。
七、实战:边属性动态更新的推荐姿势
结合源码,推荐以下两种在运行时修改边的场景化写法:
// 方式一:直接定位边的 model 并批量更新属性 const edgeModel = lf.getEdgeModelById('edge-1'); edgeModel.setProperties({ edgeWeight: true, highlight: true }); // setProperties 内部自动调用 setAttributes(),样式钩子随之重新求值 // 方式二:改变类型默认主题(全局生效) lf.updateTheme({ polyline: { stroke: '#1677ff' } });禁止直接写edgeModel.style.stroke = 'red'或edgeModel.pointsList.push(...)这类字段直写——前者绕过了主题管道,后者绕过了正交化与路径重算,都会破坏边的一致性。若确实需要直接设置样式,框架提供了专门的动作方法setStyle(key, val)/setStyles(styles)/updateStyles(styles),注释中明确说明"主要用于插件开发中跳过自定义边的渲染,多数情况下请使用getEdgeStyle"。
八、源码索引
- 数据模型与样式钩子:BaseEdgeModel.ts
- 三种内置边型:LineEdgeModel.ts、PolylineEdgeModel.ts、BezierEdgeModel.ts
- 类型定义:EdgeConfig / EdgeData、MainTypes.en.md
- 主题默认值:theme.ts
- 可运行示例:polyline.tsx、animatePolyline.tsx、curvedPolyline.tsx、basicEdge.tsx
总结
edgeModel 是 LogicFlow 边能力的统一入口:数据属性(EdgeConfig/EdgeData)描述"边是什么",形状属性(端点、拐点、控制点)描述"边怎么走",样式钩子(getEdgeStyle/getTextStyle/getArrowStyle/getOutlineStyle/getEdgeAnimationStyle)描述"边长什么样"。掌握"通过公开 API 变更状态、通过重写钩子定制样式"这两条准则,即可在不破坏框架级联计算的前提下,完成从默认折线到任意自定义边型的扩展。
【免费下载链接】LogicFlowA flow chart editing framework focus on business customization. 专注于业务自定义的流程图编辑框架,支持实现脑图、ER图、UML、工作流等各种图编辑场景。项目地址: https://gitcode.com/GitHub_Trending/lo/LogicFlow
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考