LogicFlow 节点体系完全指南:从内置 SVG 基础节点到业务自定义节点
本篇技术指南聚焦 LogicFlow 的节点(Node)体系:先认识框架内置的 7 种 SVG 基础节点及其数据模型,再深入讲解基于继承与重写的自定义节点机制(model 数据层与 view 视图层),并结合仓库源码揭示样式属性、形状属性、getShape 渲染与锚点计算之间的底层关系。读完本文,你将掌握定义符合自身业务语义的自定义节点、动态样式控制以及复杂 SVG 图形绘制的完整实战能力。
前置阅读:适合已经跑通过快速上手和实例与图数据的读者。本文不展开连接规则、锚点、React/Vue 节点方案,这些内容请继续阅读进阶节点。相关 API 参考:
nodeModel、graphModel、主题。
认识基础节点:7 种 SVG 形状
LogicFlow 是基于 SVG 实现的流程图编辑框架,节点和连线都是 SVG 基本形状。因此,对 LogicFlow 节点样式的修改,本质上就是对 SVG 基本形状的修改。框架内部共内置 7 种基础节点:
| 序号 | 节点 | 底层 SVG 元素 | 对应 view 实现 | 对应 model 实现 |
|---|---|---|---|---|
| 1 | 矩形 | rect | RectNode.tsx | RectNodeModel.ts |
| 2 | 圆形 | circle | CircleNode.tsx | CircleNodeModel.ts |
| 3 | 椭圆 | ellipse | EllipseNode.tsx | EllipseNodeModel.ts |
| 4 | 多边形 | polygon | PolygonNode.tsx | PolygonNodeModel.ts |
| 5 | 菱形 | polygon(由多边形派生) | DiamondNode.tsx | DiamondNodeModel.ts |
| 6 | 文本 | text | TextNode.tsx | TextNodeModel.ts |
| 7 | HTML | html | HtmlNode.tsx | HtmlNodeModel.ts |
从源码结构看,这 7 种节点的 model 全部继承自 BaseNodeModel.ts,view 全部继承自 BaseNode.tsx,最终在 view/node/index.ts 与 model/node/index.ts 中统一导出。基础节点的默认尺寸是宽 100、高 80(见 BaseNodeModel.ts 中 _width = 100、_height = 80),并带有最小 30、最大 2000 的宽高约束。
基础节点本身非常简单,但业务中对节点外观的需求千变万化。LogicFlow 提供了非常强大的自定义节点能力,支持开发者自定义各种节点。官方示例目录 sites/docs/src/tutorial/basic/node/shapes 中就有全部 7 种基础节点的可视化展示。
自定义节点:基于继承与重写
LogicFlow 通过继承来实现自定义节点和边:开发者继承内置节点,然后利用面向对象机制重写其方法。
注意:LogicFlow 推荐在实际应用场景中,所有节点都使用自定义节点,将节点的
type定义为符合项目业务意义的名称,而不是使用圆形、矩形这种仅表示外观的节点。
节点的 model 与 view
model:数据层,包含节点各种样式(边框、颜色)、形状(宽高、顶点位置)、业务属性等。view:视图层,控制节点的最终渲染效果。通过改变model就可以满足大部分自定义需求,同时可以在view上定制更复杂的 SVG 元素。
LogicFlow 基于 MVVM 模式实现。在自定义一个节点时,可以重新定义节点的 model 和 view:通过重写定义在 model 上获取样式相关的方法,以及重写 view 上的 getShape,来定义复杂的节点外观。
从源码看,MVVM 的实现依托 preact 与 mobx:model 中的属性大量使用 @observable 装饰器(如 BaseNodeModel.ts 中的 x、y、width、height、isSelected 等),保证数据变化能自动驱动视图更新;view 则以 preact 组件的形式渲染 SVG。如果熟悉 React 开发,阅读 packages/core 源码的难度与阅读自己的项目相当。
注册自定义节点使用 lf.register。从 LogicFlow.tsx 的源码可以看到,register 支持两种方式:
- 方式一(推荐):传入
{ type, view, model }配置对象; - 方式二:
lf.register(type, fn),通过回调函数拿到{ RectNode, RectNodeModel }等基类后再返回{ view, model },适合极个别需要引用lf实例的场景。
以 @logicflow/extension 中提供的可缩放节点为例:基础节点不支持缩放,extension 包在基础节点之上封装了缩放逻辑后发布出去,开发者可以直接基于 extension 中的可缩放节点进行二次自定义:
import { RectResize } from "@logicflow/extension";
class CustomNodeModel extends RectResize.model {}
class CustomNode extends RectResize.view {}
自定义节点 model
LogicFlow 把自定义节点外观分为自定义节点样式属性和自定义节点形状属性两种方式。更多详细定义方法,请查看 NodeModel API。
1. 样式属性:重写 getNodeStyle
样式属性控制节点的边框、颜色这类偏外观的属性。这些属性可以直接通过主题配置控制,自定义节点样式可以看作在主题的基础上、基于当前节点的类型再次定义。
例如:在主题中对所有 rect 节点定义边框颜色为红色 stroke: red,那么可以在自定义节点 UserTask 时,重新定义其边框为蓝色:
class UserTaskModel extends RectNodeModel {
getNodeStyle() {
const style = super.getNodeStyle();
style.stroke = 'blue';
return style;
}
}
从源码看,getNodeStyle 的合并链在 RectNodeModel.ts 中体现得很清晰:先取 BaseNodeModel.getNodeStyle() 返回的 theme.baseNode 与节点自身 style,再与主题中的 rect 配置、properties.style 逐层合并——主题 < 节点自身属性 < 节点 data 中的 properties.style。
2. 形状属性:重写 setAttributes / initNodeData
形状属性指节点的宽 width、高 height,矩形的圆角 radius、圆形的半径 r、多边形的顶点 points 等控制节点最终形状的属性。因为 LogicFlow 在计算节点锚点、连线起点终点时会基于形状属性计算,所以形状属性的自定义必须在 setAttributes 方法或 initNodeData 方法中进行:
class customRectModel extends RectNodeModel {
initNodeData(data) {
super.initNodeData(data);
this.width = 200;
this.height = 80;
this.radius = 20; // 矩形特有
}
// or
setAttributes() {
this.width = 200;
this.height = 80;
this.radius = 20; // 矩形特有
}
}
这两个方法在 BaseNodeModel.ts 的构造函数中被依次调用,且注释明确说明二者的区别:initNodeData 只在节点初始化时调用;setAttributes 除了初始化时调用外,每次 properties 变化时也会触发。这也是为什么基于 properties 响应式更新形状需要写在 setAttributes 中。
LogicFlow 对不同基础节点存在各自特有的形状属性:
- 矩形:
radius(圆角,默认 0,见 RectNodeModel.ts); - 圆形:
r(半径); - 椭圆:
rx、ry; - 多边形:
points(顶点数组)。默认points就是菱形[[50,0],[100,50],[50,100],[0,50]](见 PolygonNodeModel.ts),并会在设置width/height时通过normalizePolygon归一化顶点坐标; - 菱形:
rx、ry(默认 30 / 50,见 DiamondNodeModel.ts)。
详情见 NodeModel API 形状属性。
注意:如果不在
model中设置形状属性,而是直接在view中定义生成图形的宽高等形状属性,会出现锚点位置、outline 大小不正确的情况,连线的位置也可能出现错乱。因为锚点、外边框、连线端点都是基于model中的width和height生成的。
3. 基于 properties 属性自定义节点样式
在图数据中曾提到:不论是节点还是边,LogicFlow 都保留了 properties 字段,它既可以用于修改元素的样式、形状属性,也用于存放开发者的业务属性。因此,可以在自定义节点时基于 properties 数据属性控制节点显示不同样式。
下面这个例子展示了业务属性 statu 驱动边框颜色的完整实现(源码见 properties/customRect.ts):
import { RectNode, RectNodeModel } from '@logicflow/core';
class CustomRectModel extends RectNodeModel {
getNodeStyle() {
const style = super.getNodeStyle();
const properties = this.properties;
if (properties.statu === 'pass') {
style.stroke = 'green'; // 业务属性 statu 为 'pass' 时展示绿色边框
} else if (properties.statu === 'reject') {
style.stroke = 'red'; // 业务属性 statu 为 'reject' 时展示红色边框
} else {
style.stroke = 'rgb(24, 125, 255)';
}
return style;
}
}
class CustomRectNode extends RectNode {}
export default {
type: 'custom-rect',
view: CustomRectNode,
model: CustomRectModel,
};
对应的图数据(见 properties/data.ts)把业务属性、形状属性、样式属性都放在 properties 中:
{
id: '2',
type: 'custom-rect',
x: 300,
y: 100,
text: 'pass',
properties: {
statu: 'pass', // 业务属性
width: 100, // 形状属性
height: 100,
radius: 20,
style: { // 样式属性
strokeWidth: 3,
},
},
},
提示:如果发现
this.properties打印出来是一个 Proxy 对象、无法直接看到属性,这是 mobx 的响应式代理所致,可以使用{ ...this.properties }展开后打印。
自定义节点 view:重写 getShape
自定义节点的 model 可以定义基础形状、样式等属性,但当需要一个更复杂的节点时,可以使用自定义节点 view 的方式:重写 getShape 方法。
getShape 的作用是定义最终渲染的图形,LogicFlow 内部会将其返回的内容插入到 SVG DOM 上。开发者不是一定要重写此方法,只有在期望改变最终渲染图形 SVG DOM 时才需要使用它。例如 rect 节点最终渲染的 SVG DOM 只是一个矩形,但想在上面加一个图标时,就必须修改最终渲染图形的 SVG DOM,这时就需要重写 getShape。
h 渲染函数
getShape 的返回值会用到 h 函数。h 是 LogicFlow 对外暴露的渲染函数,其用法与 React、Vue 的 createElement 一致。这里需要创建的是 SVG 标签,所以需要一定的 SVG 基础知识。基本语法为 h(nodeName, attributes, [...children]),举几个例子:
h(nodeName, attributes, [...children])
// <text x="100" y="100">文本内容</text>
h('text', { x: 100, y: 100 }, ['文本内容'])
/**
* <g>
* <rect x="100" y="100" stroke="#000000" strokeDasharray="3 3"></rect>
* <text x="100" y="100">文本内容</text>
* </g>
*/
h('g', {}, [
h('rect', { x: 100, y: 100, stroke: "#000000", strokeDasharray: "3 3"}),
h('text', { x: 100, y: 100 }, ['文本内容'])
])
一个结合 model 与 view 的完整示例位于 sites/docs/src/tutorial/basic/node/custom-view,其中 userTask.ts 通过 getShape 在矩形内叠加了一个用户图标。
节点外观的三种定义方式与优先级
LogicFlow 定义一个节点的外观有三种方式,优先级为 主题 < 自定义节点 model < 自定义节点 view:
- 主题:定义所有此基础类型节点的通用样式,例如定义所有
rect节点的边框颜色、宽度等; - 自定义节点 model:定义此注册类型节点的数据,存储和管理该节点样式、形状、业务等数据属性信息;
- 自定义节点 view:定义此注册类型节点的 SVG DOM,基于
model的属性将数据渲染成用户可见的图形化形式。
注意:虽然自定义节点
view优先级最高、功能也最完善,理论上可以完全通过它实现任何想要的效果,但此方式仍存在限制:
view最终生成的图形的形状属性必须和model中的形状属性一致,因为节点的锚点、外边框都是基于model中的width和height生成;view最终生成的图形整体轮廓必须和继承的基础图形一致,不能继承rect却在getShape时返回circle轮廓,因为 LogicFlow 对节点上的连线调整、锚点生成会基于基础图形进行计算。
深入思考:基础形状的坐标语义
1. 为什么 rect 的 x、y 不是直接从 model 中获取?
在 LogicFlow 所有基础节点中,model 里的 x、y 统一表示元素中心点坐标。但 getShape 直接生成 SVG DOM 时,不同元素类型对 x、y 的语义不同:
rect:x、y表示图形左上角坐标,所以一般要用中心点减去宽高的一半来计算左上角坐标:
const { x, y, width, height, radius } = this.props.model
// svg dom <rect x="100" y="100" width="100" height="80">
h('rect', {
...style,
x: x - width / 2,
y: y - height / 2,
rx: radius, // 注意这里是 rx 属性而不是 radius
ry: radius,
width,
height,
})
这一转换在内置 Rect.tsx 中同样存在:leftTopX = x - width / 2、leftTopY = y - height / 2,并在设置 radius 时自动映射为 SVG 的 rx/ry(见 Rect.tsx)。
自定义矩形的 view 时 radius 设置:在
model中,radius是矩形节点的形状属性。但 SVG 中设置矩形圆角使用的是rx、ry而非radius,所以自定义view的矩形时,需要把model中radius的值赋给rx和ry,否则圆角不生效。
circle和ellipse:通过cx、cy表示位置,含义就是中心点坐标,无需转换:
const { x, y, r } = this.props.model;
// svg dom <circle cx="100" cy="100" r="20">
h("circle", {
...style,
r, // 半径保持不变
cx: x,
cy: y,
})
// 椭圆
const { x, y, rx, ry } = this.props.model
// svg dom <ellipse cx="100" cy="100" rx="20" ry="10">
h('ellipse', {
...style,
cx: x,
cy: y,
rx,
ry,
})
polygon:所有顶点坐标已包含位置信息,需要把model的points相对坐标加上中心点x、y:
const { x, y, points } = this.props.model
const pointStr = points.map((point) => {
return `${point[0] + x}, ${point[1] + y}`
}).join(' ')
// svg dom <polygon points="100,10 250,150 200,110" >
h('polygon', {
...style,
points: pointStr,
})
2. props 怎么用?
LogicFlow 基于 preact 开发,自定义节点 view 时可以通过 this.props 获取父组件传递的数据。this.props 对象包含两个属性:
model:当前节点的 model;graphModel:整个 LogicFlow 图的 model,API 见 graphModel。
3. 图标的 path 如何获取?
一般情况下,图标可以找 UI 或从图标平台获取一个 SVG 格式文件,然后用 IDE 以文本方式打开并格式化,即可看到代码。代码中一般是最外层一个 svg 标签,内部是一个或多个 path,此时用前面提到的 h 方法复现即可。
SVG 标签一般包括如下属性:
viewBox:允许指定一组图形伸展以适应特定容器元素,一般直接把 SVG 标签上的viewBox属性值复制过来即可;width和height:不需要使用 SVG 文件里的原始值,直接写成期望的宽高即可。
path 标签属性:
d:定义路径的数据,直接复制 SVG 代码即可,无需关心d具体内容的含义;fill:路径的填充颜色,一般和节点的边框颜色一致,也可以按业务需求自定义。
userTask.ts 中的 getLabelShape 就是标准做法:外层 svg 设置 viewBox: '0 0 1274 1024' 与期望的宽高,内部两个 path 分别设置 fill: style.stroke(跟随节点边框色)与从图标文件复制的 d 数据。
源码级实战:一个完整的业务节点
将以上知识点串起来,参考 userTask.ts 的完整 UserTask 节点,可以看到业务节点开发中常重写的全部钩子方法:
import { RectNode, RectNodeModel, h } from '@logicflow/core';
class UserTaskView extends RectNode {
getShape() {
const { model } = this.props;
const { x, y, width, height, radius } = model;
const style = model.getNodeStyle();
return h('g', {}, [
h('rect', {
...style,
x: x - width / 2, // 矩形默认 x/y 是左上角顶点,这里切换为中心点
y: y - height / 2,
rx: radius,
ry: radius,
width,
height,
}),
this.getLabelShape(), // 自定义叠加的图标
]);
}
}
class UserTaskModel extends RectNodeModel {
setAttributes() {
const { scale = 1, width = 100, height = 80 } = this.properties;
this.width = width * scale; // 手动设置形状属性
this.height = height * scale;
}
getTextStyle() { /* 依赖业务属性 isClicked 改变文本颜色 */ }
getNodeStyle() { /* 依赖业务属性 isClicked 改变边框颜色 */ }
getAnchorStyle() { /* 自定义锚点样式 */ }
getAnchorLineStyle() { /* 自定义锚点拖出连线样式 */ }
getOutlineStyle() { /* 自定义选中轮廓框样式 */ }
}
export default {
type: 'UserTask',
view: UserTaskView,
model: UserTaskModel,
};
这个例子完整覆盖了本文的四个主题:setAttributes 中定义形状属性、getNodeStyle/getTextStyle 中定义样式属性、基于 properties 的业务状态驱动、getShape 中定义复杂 SVG DOM,同时它还重写了 getAnchorStyle、getAnchorLineStyle、getOutlineStyle 来分别定制锚点、拖出连线和选中轮廓——这些钩子方法均定义于 BaseNodeModel.ts,统一标注 @overridable 支持重写。
小结与后续
本文从内置的 7 种 SVG 基础节点出发,完整介绍了基于继承的自定义节点机制:model 层负责管理样式、形状与业务数据(通过 getNodeStyle、setAttributes/initNodeData、properties 三个入口),view 层通过 getShape 与 h 函数定义最终渲染的 SVG DOM,三者优先级为 主题 < 自定义节点 model < 自定义节点 view。掌握这些内容后,即可根据业务需要创建任意外观的业务节点。
更复杂的连接规则、锚点、框架节点以及 React/Vue 节点方案,请继续阅读进阶节点(React 节点、Vue 节点)。相关 API 细节可查阅 nodeModel 与 register。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考




