ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LogicFlow nodeModel 完全指南:节点数据模型、属性分类与自定义节点核心 API

LogicFlow nodeModel 完全指南:节点数据模型、属性分类与自定义节点核心 API LogicFlow nodeModel 完全指南节点数据模型、属性分类与自定义节点核心 API【免费下载链接】LogicFlowA flow chart editing framework focus on business customization. 专注于业务自定义的流程图编辑框架支持实现脑图、ER图、UML、工作流等各种图编辑场景。项目地址: https://gitcode.com/GitHub_Trending/lo/LogicFlow本文围绕 LogicFlow 的运行时 Model 层核心概念nodeModel展开系统梳理节点的数据属性、状态属性、形状属性、其它属性四大分类并逐一讲解initNodeData、setAttributes、getNodeStyle、连线/移动规则等可重写方法与常用实例方法的签名、用途与真实源码实现。读完本文你将掌握节点的一切操作本质都是对 model 的操作这一数据驱动思想并能基于 nodeModel 定制任意业务节点。为什么需要理解 nodeModelLogicFlow 中所有节点都会有一个nodeModel与其对应。由于框架采用数据驱动视图的机制我们对节点的所有操作事实上都是对 model 的操作拖拽、选中、连边、修改文本、修改样式最终都会落到nodeModel的某个属性或方法上。因此理解 nodeModel 是自定义节点、编写插件、做图数据持久化的前提。官方在文档中特别强调了一个重要原则大多数情况下我们不建议直接对 nodeModel 的属性进行赋值操作而是调用 model 或者 graphModel 上提供的方法。:::error{title警告} 在对 LogicFlow 内部源码不熟悉的情况下对 model 的属性进行赋值操作可能会引起很多不符合预期的问题。例如在 model 中x、y表示节点的位置如果想要移动节点直接修改x、y的话会出现节点被移动了而节点上的文本、节点相连的边都没有动。所以想要移动节点最好的方法还是调用graphModel上的moveNode方法来实现。 :::这一警告在源码中有直接印证。GraphModel.ts 中moveNode的实现并不只是修改坐标它先通过nodeModel.getMoveDistance(deltaX, deltaY, isIgnoreRule)计算实际可移动距离会执行移动规则校验、同步移动节点文本再调用this.moveEdge(nodeId, deltaX, deltaY)让与该节点相连的所有边同步移动// 1) 移动节点 const node this.nodesMap[nodeId] const nodeModel node.model ;[deltaX, deltaY] nodeModel.getMoveDistance(deltaX, deltaY, isIgnoreRule) // 2) 移动边 this.moveEdge(nodeId, deltaX, deltaY)这解释了为什么直接改 x/y会让文本与边掉队——正确做法永远是走 model/graphModel 提供的封装方法。nodeModel 上的属性很多按用途可以分为四类数据属性、状态属性、形状属性、其它属性。数据属性持久化的核心字段节点的数据属性是 LogicFlow 图数据中用于标识节点的数据。在流程图保存时一般只保存节点的数据属性。名称类型是否必须描述idstring✅节点 idtypestring✅节点类型xnumber✅节点中心 x 轴坐标ynumber✅节点中心 y 轴坐标textTextObject/string节点文本propertiesObject包含节点样式、形状属性和业务自定义属性其中text以TextObject形式存在时其结构如下名称类型是否必须描述valuestring文本内容xnumber✅文本中心 x 轴坐标ynumber✅文本中心 y 轴坐标draggableboolean✅文本是否允许被拖动调整位置保存时不会保存此属性editableboolean✅文本是否允许被双击编辑保存时不会保存此属性可以看到draggable与editable属于运行时交互配置保存时会被剔除。对应地在 BaseNodeModel.ts 的formatText中LogicFlow 会从graphModel.editConfigModel读取全局的nodeTextDraggable、nodeTextEdit作为文本默认的拖拽/编辑能力再与传入的text合并最终把标准化的TextConfig写回data.text。从源码看getData()BaseNodeModel.ts就是保存时返回的数据的最终形态它输出{ id, type, x, y, properties }并且仅在存在文本value时才带上text仅在开启INCREASE/STATIC堆叠模式时才带上zIndex有旋转时带上rotate。状态属性细粒度样式与交互判断状态属性一般用于自定义节点的时候基于状态属性进行更细粒度的样式显示。名称类型是否必须描述isSelectedboolean✅节点是否被选中isHoveredboolean✅节点是否在 hover 状态isHitableboolean✅节点是否可点击draggableboolean✅节点是否可拖动isShowAnchorboolean✅是否显示锚点isDraggingboolean✅是否在拖动visibleboolean✅是否显示1.1.0新增在 BaseNodeModel.ts 中可以看到这些状态属性的默认值observable isSelected false observable isHovered false observable isShowAnchor false observable isDragging false observable isHitable true observable isHittable true // 细粒度控制节点是否对用户操作进行反应 observable draggable true observable visible true这里值得注意的细节源码中同时存在isHitable与isHittable两个字段——注释明确说明isHitable是为了兼容历史拼写错误而保留的BaseModel.ts。此外setHovered在置位 hover 的同时会自动联动setIsShowAnchor见 BaseNodeModel.ts即悬停节点自动显示锚点。形状属性决定基础节点外观LogicFlow 的形状属性主要控制基础节点的主要外观。形状属性可以通过setAttributes或者initNodeData来设置。具体设置方式见自定义节点的形状属性。名称类型是否必须描述widthnumber✅节点的宽度heightnumber✅节点的高度radiusnumber矩形节点特有节点的圆角rnumber圆形节点特有圆的半径。对于圆形节点会自动基于半径计算出节点的高度和宽度rxnumber椭圆节点和菱形节点存在水平圆角的半径。会自动基于半径计算出节点的宽度rynumber椭圆节点和菱形节点存在垂直圆角的半径。会自动基于半径计算出节点的高度points[number,number][]多边形节点特有多边形顶点。会自动基于顶点计算出节点的宽度和高度不同形状节点的属性计算在源码中有明确实现圆形节点CircleNodeModel.tswidth/height是r * 2的计算属性因此只需维护r椭圆节点EllipseNodeModel.tswidth rx * 2、height ry * 2多边形节点PolygonNodeModel.tswidth/height由points中横纵坐标的极差计算得出setAttributes会通过normalizePolygon(nextPoints, width, height)对顶点做归一化处理矩形节点RectNodeModel.tswidth/height/radius会从properties中读取并同步。其它属性框架信息与扩展挂载点LogicFlow 在 model 上还维护一些属性开发者可以通过这些属性拿到一些信息例如拿到graphModel、节点的基础 model 类型等。名称类型是否必须描述graphModelGraphModel✅整个画布对应的 model详情见 graphModelApizIndexnumber✅节点在 z 轴的高度元素重合时zIndex 高的在上面默认为 1statenumber✅元素状态不同的状态对应着元素显示效果。DEFAULT 1 默认显示TEXT_EDIT 2 此元素正在进行文本编辑ALLOW_CONNECT 4此元素允许作为当前边的目标节点NOT_ALLOW_CONNECT 5此元素不允许作为当前边的目标节点BaseTypestring✅当前 model 的基础类型对于节点则固定为node。主要用在节点和边混合的时候识别此 model 是节点还是边modelTypestring✅当前 model 的类型可取值有node,rect-nodecircle-nodepolygon-nodeellipse-nodediamond-nodehtml-nodetext-nodemoveRulesModel.NodeMoveRule[]节点被移动之前的校验规则sourceRulesModel.ConnectRule[]节点连接其它节点时的校验规则targetRulesModel.ConnectRule[]节点被其它节点连接时的校验规则autoToFrontboolean✅控制节点选中时是否自动置顶默认为 trueincomingobject✅进入当前节点的所有边和节点v1.1.4outgoingobject✅离开当前节点的所有边和节点v1.1.4virtualboolean-是否为虚拟节点默认 false。当为 true 时导出数据不会包含此元素。v1.1.24关于state枚举源码 constant/index.ts 中的定义为export enum ElementState { DEFAULT 1, // 默认显示 TEXT_EDIT, // 此元素正在进行文本编辑 SHOW_MENU, // 显示菜单废弃请使用菜单插件 ALLOW_CONNECT, // 此元素允许作为当前边的目标节点 NOT_ALLOW_CONNECT, // 此元素不允许作为当前边的目标节点 }modelType的全部取值在 constant/index.ts 中枚举除了文档列出的节点类型外还有custom-html-node以及边、图相关的类型。:::info{titlemodelType 与 type 的区别是什么?} 在自定义节点的时候type可以是开发者自定义的任何值但是在 LogicFlow 内部涉及到这个节点的计算时我们需要感知到这个节点的具体形状这个时候不能用type而是要用modelType来判断。 :::这一区分在 BaseModel.ts 的源码注释中有更直白的表述type更多是业务上的类型例如 BPMN 场景中的bpmn:start-event而modelType是外观上的类型——BPMN 的开始节点和结束节点type分别为bpmn:start-event和bpmn:end-event但它们的modelType都是circle-node因为外观都是基于圆形自定义而来。incoming/outgoing的实现依赖graphModel的查询方法BaseNodeModel.tscomputed get incoming(): GraphElements { return { nodes: this.graphModel.getNodeIncomingNode(this.id), edges: this.graphModel.getNodeIncomingEdge(this.id), } }样式属性SVG 渲染与样式方法重写LogicFlow 所有的节点最终都是以SVG DOM的方式渲染。但是除了形状属性之外所有其它属于 SVG 的属性都不会直接存在nodeModel上。当开发者想要对 SVG DOM 添加更多的 svg 属性时可以通过重写nodeModel上获取节点样式属性的方法来实现stroke、fill、strokeDasharray等 SVG 属性。这类方法默认返回主题Theme 类型说明中的对应配置全部支持重写getNodeStyle()自定义节点样式getTextStyle()自定义节点文本样式getAnchorStyle(anchorInfo)自定义节点锚点样式getAnchorLineStyle(anchorInfo)自定义节点锚点拖出连接线的样式getOutlineStyle()自定义节点轮廓框选中外框的样式getRotateControlStyle()获取当前节点旋转控制点的样式。例如将节点描边改为蓝色并加虚线class UserTaskModel extends RectNodeModel { getNodeStyle() { const style super.getNodeStyle(); style.stroke blue; style.strokeDasharray 3 3; return style; } }自定义锚点样式与 hover 效果class UserTaskModel extends RectNodeModel { getAnchorStyle(anchorInfo) { const style super.getAnchorStyle(anchorInfo); style.stroke rgb(24, 125, 255); style.r 3; style.hover.r 8; style.hover.fill rgb(24, 125, 255); style.hover.stroke rgb(24, 125, 255); return style; } }这些方法在源码中的默认实现都遵循合并主题 深拷贝防止污染全局主题的模式例如 BaseNodeModel.tsgetNodeStyle(): CommonTheme { return { ...this.graphModel.theme.baseNode, ...this.style, } }同时各形状节点会在自身getNodeStyle中继续叠加形状主题与properties.style如 RectNodeModel.ts形成baseNode → 形状主题 → 节点 properties.style → 运行时 setStyle的优先级链条。生命周期方法initNodeData 与 setAttributesinitNodeData支持重写初始化节点数据将传入的图数据data转换为节点属性所以需要调用super.initNodeData触发转换方法。在super.initNodeData之前对图数据进行处理在super.initNodeData之后对节点属性进行初始化。class UserTaskModel extends RectResize.model { initNodeData(data) { // 可以在super之前强制设置节点文本位置不居中而且在节点下面 if (!data.text || typeof data.text string) { data.text { value: data.text || , x: data.x, y: data.y 40, }; } super.initNodeData(data); this.width 100; this.height 80; } }源码中initNodeData做了几件关键事情BaseNodeModel.ts补全空properties按照自定义createId 全局idGenerator 内置createUuid的优先级生成 id调用formatText标准化文本将图数据通过assign(this, pickNodeConfig(data))合并进 model并根据overlapMode初始化 zIndex。:::info{title提示} initNodeData 和 setAttributes 都可以对 nodeModel 的属性进行赋值但是两者的区别在于initNodeData只在节点初始化的时候调用用于初始化节点的属性。setAttributes除了初始化调用外还会在 properties 发生变化时调用。 :::以上面代码为例由于节点缩放的时候会更新 properties 中的缩放后大小也就会触发setAttributes。如果在setAttributes中定义节点的初始大小的话会导致节点无法缩放。setAttributes设置 model 形状属性每次 properties 发生变化会触发。class UserTaskModel extends RectNodeModel { setAttributes() { const size this.properties.scale || 1; this.width 100 * size; this.height 80 * size; } }从源码看setAttributes的触发链路为setProperty/setProperties/deleteProperty都会进入updateProperties在更新this.properties后立即调用this.setAttributes()并触发node:properties-change事件BaseNodeModel.tsprivate updateProperties(nextProperties: P, updateKeys: string[]): void { const preProperties toJS(this.properties) this.properties nextProperties this.setAttributes() // 触发更新节点 node:properties-change 的事件 this.graphModel.eventCenter.emit(EventType.NODE_PROPERTIES_CHANGE, { id: this.id, keys: updateKeys, preProperties, properties: nextProperties, }) }因此setAttributes是实现properties 驱动形状的标准入口例如根据业务属性联动节点尺寸、颜色等。id 生成与文本格式化createId支持重写自定义节点 id 的生成规则。:::warning{title注意}请保证此方法返回 id 的唯一性。此方法为同步方法如果想要异步修改节点 id请参考相关 issuesdidi/LogicFlow#272。 :::import { v4 as uuidv4 } from uuid; class UserTaskModel extends RectNodeModel { createId() { return uuidv4(); } }源码中 BaseModel.ts 的注释同样强调此方法必须是同步方法如果想要异步修改 ID建议删除此节点后在同一位置创建一个新的节点。formatText初始化文本参数为data: LogicFlow.NodeConfig。class UserTaskModel extends RectNodeModel { formatText(data: LogicFlow.NodeConfig) { console.log(data, data); const defaultText { value: , x: data.x, y: data.y, draggable: false, editable: true, } if (!data.text) { data.text { ...defaultText } } else { if (typeof data.text string) { data.text { ...defaultText, value: data.text, } } // ... } } }数据读取getData 与 getPropertiesgetData获取被保存时返回的数据。LogicFlow 有固定的节点数据格式。如果期望在保存数据上添加数据请添加到properties上。不支持重写此方法源码中getData会被resize、历史记录等内部逻辑复用重写会破坏数据契约。const nodeModel lf.getNodeModelById(node_1); const nodeData nodeModel.getData();getProperties获取节点属性返回深拷贝避免外部直接修改内部响应式对象。不支持重写此方法。const nodeModel lf.getNodeModelById(node_1); const properties nodeModel.getProperties();锚点体系getDefaultAnchor 与锚点属性getDefaultAnchor重新设置默认锚点可以给锚点加上 id 等自定义属性用于对锚点的验证。class cNode extends RectNodeModel { // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。 getDefaultAnchor() { const { width, height, x, y, id } this; return [ { x: x - width / 2, y, name: left, id: ${id}_0 }, { x: x width / 2, y, name: right, id: ${id}_1, edgeAddable: false }, ] } }锚点属性名称类型是否必须描述xnumber✅锚点 x 坐标ynumber✅锚点 y 坐标idstring✅锚点 idedgeAddableboolean✅是否允许此锚点手动创建连线默认为 true从源码看锚点体系比文档展示的更完整getAnchorsByOffset()BaseNodeModel.ts优先使用anchorsOffset偏移量锚点没有时才回退到getDefaultAnchor()而anchorsgetter 还会把所有锚点按节点rotate旋转角做矩阵变换Matrix保证旋转后锚点坐标仍然正确BaseNodeModel.ts。相关配套方法getTargetAnchor(position: LogicFlow.Point)获取目标锚点返回值BaseNodeModel.AnchorInfo。默认实现是取离鼠标位置最近的锚点getClosestAnchor也支持通过graphModel.customTargetAnchor全局覆盖BaseNodeModel.tsgetAnchorInfo(anchorId?: string)根据锚点 id 查询锚点信息anchors获取锚点返回值LogicFlow.Point[]即const { anchors } node。连线与移动规则业务约束的核心手段getConnectedSourceRules / getConnectedTargetRulesgetConnectedSourceRules获取当前节点作为边的起始节点规则。支持重写重写时可以自定义添加额外规则。所有规则中有任一规则不满足则禁止连线。getConnectedTargetRules获取当前节点作为边的目标节点规则。支持重写重写时可以自定义添加额外规则。所有规则中有任一规则不满足则禁止连线。class EndNodeModel extends CircleNodeModel { getConnectedSourceRules(): ConnectRule[] { const rules super.getConnectedSourceRules(); const geteWayOnlyAsTarget { message: 结束节点只能连入不能连出, validate: ( source: BaseNodeModel, target: BaseNodeModel, sourceAnchor, targetAnchor ) { let isValid true; if (source) { isValid false; } return isValid; }, }; rules.push(geteWayOnlyAsTarget); return rules; } }class StartEventModel extends CircleNodeModel { getConnectedTargetRules() { const rules super.getConnectedTargetRules(); const notAsTarget { message: 起始节点不能作为边的终点, validate: () false, }; rules.push(notAsTarget); return rules; } }源码层面规则校验的实际入口是isAllowConnectedAsSource/isAllowConnectedAsTargetBaseNodeModel.ts两者都返回{ isAllPass, msg }结构即LogicFlow.ConnectRuleResult遍历规则数组并短路——任一规则validate返回 falsy 即中止并返回该规则的message。ConnectRule的类型定义在 BaseModel.ts 中其validate签名还会在开启adjustEdgeStartAndEnd后传入调整边的edgeID。动态分组插件的实战佐证在 dynamic-group/model.ts 中分组节点通过重写getConnectedTargetRules/getConnectedSourceRules追加分组节点不允许作为边的终点/起点规则并通过getDefaultAnchor在禁止手动连线时把锚点的edgeAddable置为false——这正是本文所讲三个可重写方法的组合应用也是验证规则 API 的正确用法。isAllowConnectedAsSource / isAllowConnectedAsTargetisAllowConnectedAsSource在连接边时是否允许这个节点为 source 节点边到 target 节点。参数| 名称 | 类型 | 是否必填 | 描述 | | ------| -------------- | --- | -- | | target |BaseNodeModel| ✅ | 目标节点 | | sourceAnchor |Model.AnchorConfig| ✅ | 源锚点 | | targetAnchor |Model.AnchorConfig| ✅ | 目标锚点 | | edgeId |string| - | 调整后边的 id |返回值LogicFlow.ConnectRuleResult详见 didi/LogicFlow#926。isAllowConnectedAsTarget在连线时判断是否允许这个节点为 target 节点。参数| 名称 | 类型 | 是否必填 | 描述 | | ------| -------------- | --- | -- | | source |BaseNodeModel| ✅ | 源节点 | | sourceAnchor |Model.AnchorConfig| ✅ | 源锚点 | | targetAnchor |Model.AnchorConfig| ✅ | 目标锚点 | | edgeId |string| - | 调整后边的 id |返回值LogicFlow.ConnectRuleResult详见 didi/LogicFlow#926。isAllowMoveNode是否允许移动节点。参数deltaX: number, deltaY: number返回值boolean | Model.IsAllowMove。其中IsAllowMove的结构为{ x: boolean; y: boolean }BaseModel.ts支持分别限制 X / Y 方向的移动。class UserTaskModel extends RectNodeModel { isAllowMoveNode(deltaX: number, deltaY: number) { let isAllowMoveX true let isAllowMoveY true // 处理 return { x: isAllowMoveX, y: isAllowMoveY, } } }源码中isAllowMoveNodeBaseNodeModel.ts会将节点自身moveRules与全局graphModel.nodeMoveRules合并后逐一执行返回boolean或{x, y}的联合结果。move、moveTo、getMoveDistance都通过它做移动前校验isIgnoreRule为 true 时可跳过。移动相关方法move、moveTo 与 moveTextmoveText(deltaX: number, deltaY: number)移动文本实现方式是更新text.x/text.yBaseNodeModel.ts。moveTo(x: number, y: number, isIgnoreRule: boolean false)移动到目标绝对坐标返回值boolean。this.node.moveTo(200, 100)move(deltaX: number, deltaY: number, isIgnoreRule: boolean false)相对移动返回值boolean。this.move(0, 24 / 2);getMoveDistance(deltaX, deltaY, isIgnoreRule false)计算实际允许的移动距离返回值Model.VectorType即[number, number]。注意该方法会真正执行移动x/y和文本同步位移同时返回[moveX, moveY]供graphModel.moveNode驱动边跟随移动。从源码实现BaseNodeModel.ts可以看到一个值得关注的行为move会分轴校验X 轴通过则只移动 X同步移动文本的 XY 轴通过则只移动 Y且移动后会用新的x/y重算旋转矩阵transform而moveTo是整体校验任一轴不允许则整体失败。状态与属性操作方法setSelected(isSelected: boolean true)设置选中状态。this.node.setSelected(true);setHovered(isHovered: boolean true)设置 hover 状态会联动锚点显示。setHittable(isHittable: boolean)设置节点是否可交互源码同时保留setHitable兼容拼写错误。setIsShowAnchor(isShowAnchor: boolean true)设置是否显示锚点。setElementState(state: ElementState, additionStateData?: Model.AdditionStateDataType)设置 Node | Edge 等 model 的状态内部同时维护state与additionStateDataBaseNodeModel.ts。setZIndex(zIndex: number 1)设置节点 zIndex。const nodeModel lf.getNodeModelById(node_1); nodeModel.setZIndex(999);setProperties(properties)整体设置节点 properties。注意它通过formatData归一化数据、用mapKeys计算变更键集合并会触发setAttributes与node:properties-change事件。lf.on(node:click, ({ data }) { lf.getNodeModelById(data.id).setProperties({ disabled: !data.properties.disabled, scale: 2, }); });setProperty(key: string, value: unknown)设置单个 property支持a.b.c形式的嵌套路径内部使用 lodash 的set见 BaseNodeModel.ts。deleteProperty(key: string)删除节点的某个属性。lf.on(node:click, ({ data }) { lf.getNodeModelById(data.id).deleteProperty(disabled); lf.getNodeModelById(data.id).deleteProperty(scale); });updateText(value: string)修改节点文本内容参数value为必传的文本值。const nodeModel lf.getNodeModelById(node_1); nodeModel.updateText(hello world);updateAttributes(attributes: LogicFlow.AttributesType)直接合并更新一组属性内部为assign(this, attributes)。setStyle(key: string, value: unknown)/setStyles(styles: LogicFlow.CommonTheme)/updateStyles(styles: LogicFlow.CommonTheme)运行时动态修改节点样式。其中setStyle/setStyles是合并进this.style而updateStyles是整体替换见 BaseNodeModel.ts。setEnableRotate(flag: boolean)设置是否启用旋转。实战小结自定义一个业务节点的完整模板综合以上 API一个典型的自定义业务节点通常组合使用如下能力以文档中的结束节点为例继承对应形状的 ModelCircleNodeModel/RectNodeModel等用initNodeData处理入参图数据用setAttributes让properties驱动形状如按scale缩放宽高用getNodeStyle等样式方法定制 SVG 外观用getConnectedSourceRules/getConnectedTargetRules约束连线方向用getDefaultAnchor定制锚点位置与edgeAddable用moveRules/isAllowMoveNode约束移动业务数据一律存properties用getData/getProperties读取用setProperty/setProperties/deleteProperty修改。延伸阅读运行时 Model 另一核心graphModel.zh.md主题与默认样式说明theme.zh.md自定义节点的形状属性设置node.zh.md各类形状节点 Model 的源码实现model/node 目录基础 Model 接口定义含ConnectRule、NodeMoveRule、IsAllowMove等类型BaseModel.ts动态分组插件中基于 nodeModel 规则 API 的实战案例dynamic-group/model.ts【免费下载链接】LogicFlowA flow chart editing framework focus on business customization. 专注于业务自定义的流程图编辑框架支持实现脑图、ER图、UML、工作流等各种图编辑场景。项目地址: https://gitcode.com/GitHub_Trending/lo/LogicFlow创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表