源于公司需求,需要支持嵌套工作流的绘制,画布组件库使用的:vue-flow
整体踩坑心路历程
阶段一:调研官方文档,实现 DagNode 中含 Nested Node 嵌套节点
- 查看官方文档
Nested Node的demo,发现其只有用其库中内置的节点绘制嵌套节点的案例。这意味着需要在 DagNode(自定义的节点)中去自己适配嵌套节点的形态和约束; - 官方文档中未说明子节点的 position 是对于父容器的相对位置,经过查看源代码和测试才确认;
- 用 mock 的数据成功绘制了嵌套的工作流,问题来到了画布节点布局,需要适配含子节点的布局计算;
阶段二:优化画布节点布局
- 尝试在原有dagre工具上适配嵌套,但其计算的节点布局会将所有节点平铺计算位置,不适用有嵌套节点的画布计算布局;
- 研究 elkjs,成功绘制出预期嵌套节点。少量节点的布局可控,但压力测试下大量节点计算出的布局较混乱,远不如dagre,且配置复杂不可控;
- 转回研究 dagre 适配嵌套工作流的画布布局计算,落地由内至外布局,由外至内绘制的方案,并重写优化画布绘制方案,提升画布渲染效率,减少重绘;
- 总结复盘时,发现dagre创建画布使用graphlib库,其支持配置嵌套节点...,但进一步看 issue的问题,发现其对nested node的支持还不够健壮,还有存在未解决的问题。
- 综合考虑,最终「画布节点布局 + 画布渲染」就使用自己落地的由内至外布局,由外至内绘制的方案;
- 针对现有的「绘制大量节点,工作流很高时初始画布视口可能看不到节点」的问题进行了优化,动态计算最优画布视口。
原方案绘制和布局画布
原方案是:先数据驱动将节点绘制完毕之后,再布局计算去updateNode,这样其实相当于是将节点渲染绘制两次,先以不正确的位置将节点绘制到画布上,再计算布局得到正确位置之后挪动节点,这样会造成资源浪费,浏览器重绘
最终布局计算+画布渲染方案
1. 初始化渲染整体流程
添加节点/复制粘贴/点击优化布局后画布局部渲染的整体流程与初始化渲染类似,处理子流程和主流程的步骤完全一致,只是第三步改为遍历所有节点,dagre布局计算得到的位置和当前节点位置不一致的话,就需要去更新节点updateNode的的位置和宽高样式
2. 父节点宽高计算
父容器宽高计算:
宽 = maxX - minX + 子节点宽 + PARENT_PADDING * 2 + CHILD_PADDING * 2
高 = maxY - minY + 子节点宽 + PARENT_PADDING * 2 + CHILD_PADDING * 2 + HEADER_HEIGHT(父容器Header高度)
构建子流程节点画布数据 - 计算子节点相对父节点的位置
注意点:
- dagre.js 布局计算后返回的是节点中心点的坐标,而vue-flow绘制时是基于节点
左上角的坐标; - 在vue-flow中子节点的position是相对父节点的
相对位置;
子节点的相对位置计算:
locX = subX - nodeWidth / 2 - marginX + PARENT_PADDING+ CHILD_PADDING
locY = subY - nodeHeight / 2 - marginY + PARENT_PADDING+CHILD_PADDING+HEADER_HEIGHT
4. 动态计算最优视口位置
最优视口定义:
计算最优视口框架:
- 将左1/4区域的位置视作最优视口区域,如果没有任何节点在这区域之中,则需要调整画布的视口
- 因为dagre依据从左至右布局计算时,最左侧节点距离画布左侧的距离一定为设置marginx,所以
视口的x坐标无需调整; - 视口的调整即相当于把画布向上拖动,让节点调整到左上方期望的位置,所以
视口的y坐标为负值;
视口y坐标 = (-targetNode.y+targetNode.height/2+ marginY) * 视口缩放倍数