引言
Hi,大家好,上周发布了LogicFlow动画边实现解析的文章得到了很多朋友的点赞,在这里感谢上周大家对我们创作内容的认可。最近一段时间我们有一些人力投入在了制作一个新手程序员都能看懂的流程图渲染原理解析视频上,今天它终于问世啦!欢迎大家点击下面的视频一起了解流程图渲染原理~如果你觉得这个视频和这篇文章对你有帮助的话,请为我们的项目点上star⭐️这对我们真的很重要!!!!(破音
考虑到有些朋友可能想再深入一点了解流程图框架的工作原理,这里就以LogicFlow为例带大家深入剖析。
框架结构
LogicFlow 基于 MVVM 模式构建,采用 SVG 与 Preact 来渲染和更新视图,数据层则使用 MobX 进行状态管理,确保数据和视图的双向绑定,响应变化自动更新界面。
多图层的View
在视图层中,整个画布被分为五层,分别用于渲染不同类型的内容,确保画布内容的层次分明,并提供灵活的展示方式:
层级 | 用途 |
---|---|
工具层 | 容纳插件,像拖拽面板、菜单、以及文本编辑框都放在这里 |
调整层 | 容纳辅助画布操作的元素,像节点和边选中时展示的外框、曲线边的支点、参考线都放在这里 |
元素层 | 容纳流程图的主要元素,节点和边就放在这里 |
网格层 | 展示网格 |
背景层 | 展示背景图片,初始化时配置了背景图片才会展示 |
高灵活度的Model
为了提高复用性和灵活性,LogicFlow分别为节点和边设计了通用的基础节点类:BaseNodeModel和基础边类:BaseEdgeModel,在此基础上衍生提供7种节点和3种边,而画布的能力则抽象拆分出画布类:GraphModel、控制画布编辑状态的状态控制类EditConfigModel和控制画布缩放比例,偏移距离的画布调整类:TransformModel。
交互层❎ViewModel✅
在 LogicFlow 中,视图模型(ViewModel)负责响应模型的数据变化并更新到视图。通过 MobX 和 Preact 的组合,模型的变化会直接触发视图的自动更新,使开发者可以专注于数据逻辑,减少手动更新的复杂度。考虑到深入讲解,篇幅会比较长,本期暂时不深入介绍。
画布渲染
在初始化阶段,LogicFlow 的渲染过程包括以下四步:
-
挂载容器:LogicFlow 实例化时,开发者通过 container 参数传入目标 DOM,LogicFlow 在该容器中创建画布。
-
构建画布:实例化 GraphModel,设置画布的宽高、样式主题和初始参数(如事件中心、实例 ID、边类型等)。
-
注册元素:遍历内置的节点和边类型,将其挂载到 GraphModel 实例中,并加上事件监听。
-
渲染画布内容:当执行 lf.render() 时,开始渲染数据,主要分为 数据转换 和 画布渲染 两部分。
挂载容器
LogicFlow实例化时会要求开发者通过container参数传入容器DOM,LogicFlow会创建一个画布的容器,塞到这个DOM下
构建画布
接着实例会调用 new GraphModel() 触发画布的实例化。
画布实例化其实主要做三个事情:
- 根据LogicFlow传入的参数设置画布宽高,如果参数中没有宽高,就采用容器的宽高
- 设置画布的样式主题
- 初始化其他信息,例如事件中心、实例Id和边类型
注册元素
然后是注册元素,这一步主要是遍历内部预设好的10种画布元素类,为它们加上监听后挂到grapgModel实例上。
小知识:开发者们手动调用lf.register()方法注册元素其实就是单独执行了一下这个环节中遍历执行的逻辑。
渲染画布内容
至此,整个实例就已经准备好了,当逻辑运行到lf.render()时,就开始渲染数据了。这里主要分为两步:数据转化和画布渲染。
数据转换
在渲染前,LogicFlow 会将传入的数据转换为一个个节点和边实例,如下图所示:
首先,LogicFlow 遍历所有节点和边数据,根据类型检查是否已注册对应的类型,若未注册则抛出异常提示。成功转换后的数据被存储到画布中,以便进一步渲染。
画布渲染
数据转换完成后,LogicFlow 调用内部的 render 方法,将节点和边数据传递给相应的 SVG 组件,完成最终的画布呈现。
画布更新
画布的监听
目前LogicFlow的画布为元素层增加了一些鼠标事件的监听,它们主要是用于拖拽创建时响应鼠标动作
{
onMouseEnter: this.dragEnter, // 鼠标移入
onMouseOver: this.dragEnter, // 鼠标移入
onMouseMove: this.onDragOver, // 鼠标移动
onMouseLeave: this.onDragLeave, // 鼠标移出
onMouseUp: this.onDrop, // 鼠标放开
}
元素的监听
LogicFlow中所有继承了BaseNode和BaseEdge的元素默认都继承了这两个基础类为DOM添加的监听事件。
接下来借着视频里使用的拖拽创建节点和边的例子来带大家梳理一下LogicFlow画布更新都做了什么
示例解析:拖拽创建节点和边
拖拽创建节点和边的流程按鼠标操作路径可以拆分成以下六个动作:
在拖拽图形上按下 → 拖拽到某个位置 → 鼠标放开 → 移入节点 → 在锚点上按下并拖拽 → 鼠标放开
拖拽创建节点
在拖拽图形上按下
当鼠标在拖拽图形上按下时,拖拽面板就会调用lf.dnd.startDrag方法,把当前点击图形对应的节点配置存储下来,触发全局监听鼠标放开事件。
你可能会好奇这里出现的个新概念lf.dnd是什么,它其实是lf实例中svg画布操作事件的集合,里面包含了进入画布、在画布中移动、离开画布、在画布上放开鼠标等动作的响应事件
拖拽移动到某个位置
鼠标拖拽图形移动到某个位置的动作,对画布来说其实算两个甚至三个动作:鼠标移入画布 → 鼠标在画布上移动 → 如果鼠标移出了画布视口,那就还有第三个动作鼠标移出画布。
因为拖拽面板展示在工具层,而响应鼠标动作,创建节点跟随鼠标移动的事件则需要在鼠标进入元素层范围时才会触发,因此鼠标从拖拽面板中移到画布上的那一刻,才会触发lf.dnd.dragEnter事件,调用lf.createFakeNode方法,在鼠标当前位置创建一个虚拟节点跟随鼠标。
鼠标在画布上移动时事件会持续触发lf.dnd.onDragOver移动虚拟节点,保障虚拟节点与鼠标位置一致。
如果鼠标移出了画布视口,则会触发lf.dnd.onDragLeave方法,这个虚拟节点会直接移除。
鼠标放开
鼠标放开的那一刻,虽然在页面上不会有什么变化,但画布会触发lf.dnd.onMouseUp方法,拿着虚拟节点的数据创建出真实节点放到鼠标所在的位置上。
拖拽创建边
移入节点
LogicFlow为节点提供了一些状态属性
readonly virtual: boolean = false // 是否是虚拟节点
@observable isSelected = false // 节点是否可选中
@observable isHovered = false // 节点是否获焦
@observable isShowAnchor = false // 是否展示锚点
@observable isDragging = false // 是否正在拖拽
@observable isHitable = true // 是否可点击
@observable draggable = true // 是否可拖拽
@observable visible = true // 是否可见
@observable rotatable = true // 节点是否可旋转
@observable resizable = true // 节点是否可缩放
在实际渲染时 一个节点的DOM结构是这样的:
当鼠标移入节点容器时会触发容器的mouseEnter事件,这个事件执行时会把节点的isHovered设置为true,Mobx观测到isHovered属性变为true,把变化广播出去,节点的外边框和锚点组件接收到变更,修改显隐逻辑,外边框和锚点就显示出来了。
在锚点上并拖拽
LogicFlow的锚点其实也是单独的组件,当鼠标在锚点上按下并拖拽时,会触发锚点组件上配置的拖拽监听事件,创建虚线跟随鼠标移动。内部的实现感兴趣的小伙伴可以直接了解源码,这里主要结合锚点的拖拽为大家介绍一下在锚点背后,LogicFlow设计的拖拽工具类 StepDrag。
锚点组件的构造函数执行时会调这样一段代码:
this.dragHandler = new StepDrag({
onDragStart: this.onDragStart,
onDragging: this.onDragging,
onDragEnd: this.onDragEnd,
})
这里new StepDrag创建的就是属于这个锚点的拖拽工具,它内部实现大概是这样的:
export class StepDrag {
constructor({
onDragStart = noop,
onDragging = noop,
onDragEnd = noop,
...
}: IStepperDragProps) {
this.onDragStart = onDragStart
this.onDragging = onDragging
this.onDragEnd = onDragEnd
...
}
setStep(step: number) {
this.step = step
}
setModel(model: Model.BaseModel) {...}
handleMouseDown = (e: MouseEvent) => {...}
handleMouseMove = (e: MouseEvent) => {...}
handleMouseUp = (e: MouseEvent) => {...}
cancelDrag = () => {...}
destroy = () => {...}
}
它接收的参数主要是对拖拽事件的定义,比如 开始拖拽、拖拽中、拖拽完成都要做什么事情,定义上报事件名,拖拽步长是什么样的等等。
它为鼠标在元素上按下、拖拽和放开三个动作提供了固定的执行流程,只要调用stepDrag的handlerMouseDown,它就会自己执行记录当前点击的位置和元素数据,增加鼠标移动和放开事件的监听,上报事件等动作。
handleMouseDown = (e: MouseEvent) => {
const DOC: any = window?.document
if (e.button !== LEFT_MOUSE_BUTTON_CODE) return
if (this.isStopPropagation) e.stopPropagation()
this.isStartDragging = true
this.startX = e.clientX
this.startY = e.clientY
DOC.addEventListener('mousemove', this.handleMouseMove, false)
DOC.addEventListener('mouseup', this.handleMouseUp, false)
const elementData = this.model?.getData()
this.eventCenter?.emit(EventType[`${this.eventType}_MOUSEDOWN`], {
e,
data: this.data || elementData,
})
this.startTime = new Date().getTime()
}
同时也支持中断拖拽,只需要调用cancelDrag就行。
cancelDrag = () => {
const DOC: any = window?.document
DOC.removeEventListener('mousemove', this.handleMouseMove, false)
DOC.removeEventListener('mouseup', this.handleMouseUp, false)
this.onDragEnd({ event: undefined })
this.isDragging = false
}
而需要使用这个拖拽流程的元素类就只需要像锚点这样在构造的时候把各个动作触发时需要做的事情传给StepDrag,创建出实例并存储下来,在适当的时机调用它的handleMouseDown方法,就可以简单低成本地实现一段拖拽逻辑。
鼠标放开
在鼠标放开时,StepDrag会第一时间触发锚点的onDragEnd方法,在这个方法里,锚点会先根据鼠标放开时所在的坐标找到对应的节点,如果这里没有找到对应的节点,就结束,如果找到了对应的节点,就取当前锚点所在节点的信息和对应节点的信息,先判断两个节点是否支持作为起点/终点创建连线,如果不支持则抛出不允许连线的事件,如果支持则以默认边类型创建一条边。
至此整个流程就结束了。
结语
以上是LogicFlow流程图渲染渲染和更新的原理解析,无论是新手程序员还是有经验的开发者,都欢迎深入了解 LogicFlow 的工作原理。希望这篇文章能帮助你理解并深入掌握 LogicFlow 的使用和开发。 也欢迎大家加入到LogicFlow的共建中来~
关于流程图框架大家还有什么想了解的欢迎在评论区留言,我们放到后续的文章更新计划中~