我做了好几年的前端可视化方向,遇到过产品拿着竞品截图提需求、也遇到过研发同事抱怨差点把图编辑器做成了另一个画板。今天想聊聊我们最近这个代号为 diagram-design 的自研图编辑器项目,不是那种套壳产品,而是从数据结构、交互、渲染到工程化完整跑通的一整套方案,它解决的核心问题就是:当你需要让用户在一个画布上自由绘制节点、连线、整理逻辑关系时,怎么做到既好用又不卡、既灵活又可控。现在很多业务都离不开这种能力,比如低代码平台、流程图工具、组织架构编辑、系统架构设计器,甚至直播间的观众连麦关系图。如果你正打算做类似的东西,或者已经在泥潭里挣扎了,这篇内容应该能让你少踩不少坑。
标题里的 diagram-design 其实就是整个项目的代号,听起来像个设计稿目录名,但真正做起来,它横跨了数据建模、图形渲染、交互算法、性能优化和前端工程化好几个层次。它不是那种“找现成库改一改”的玩具项目,而是真正从业务场景里长出来的、能承载真实生产环境压力的图编辑基础设施。接下来我把整个设计思路、核心模块、实操过程和踩坑记录都整理出来,希望对你有用。
1. 整体设计与架构思路
1.1 为什么没有直接套开源方案
做图编辑器,很多人第一反应就是找现成的库,比如某些知名的流程图框架或者图形库。确实,开源库能覆盖一部分基础场景,但现实业务里,需求永远比开源库默认能力走得更远。我们最初也尝试过基于某个开源库做二次开发,但很快遇到几个比较尴尬的问题:一是交互定制成本高,很多底层事件绑定和渲染逻辑是写死的,想实现“节点内部再嵌套一个小型泳道图”这种需求,得翻源码改核心逻辑;二是数据序列化格式是被框架绑定的,一旦业务想把自己的领域模型直接映射成图数据,中间就隔了一层转换,而且反向转换会丢字段;三是渲染性能在大规模节点下波动明显,尤其是当节点数量过千、连线关系复杂时,整体构建和更新效率都不太可控。
在某次内部评审会上,我们拿到了一个很典型的业务需求:要在一个可视化编排平台里,让用户拖拽出一张完整的服务调用关系图,节点要支持多端口、端口可以自定义位置、连线要能智能避让、整张图要能导出成 JSON 给后端做配置下发,同时还要支持从 JSON 反向渲染回画布。这套需求组合拳打下来,开源库差不多是全军覆没。于是我们决定自研 diagram-design,最核心的目标就是三个:数据结构完全独立、交互逻辑完全可控、渲染层可替换。
1.2 领域模型驱动还是视图驱动
架构设计第一步,其实就是回答一个问题:图数据到底怎么组织。我把设计理念定成了“领域模型驱动”,也就是先把图数据抽象成一张独立的领域模型,再让渲染层和交互层去适配这张模型,而不是反过来被渲染层的数据结构绑架。整张图模型包含三个核心实体:节点(Node)、端口(Port)和连线(Edge)。节点就是画布上的一个对象,比如一个服务、一个审批节点、一个直播间观众;端口是节点上的连接点,用来给连线提供锚定的位置;连线则是从源端口到目标端口的一条关系线。
这套模型听起来很简单,但真正落地时,难点在于端口的位置和连线的路由方式。传统方案里,端口位置是写死在渲染层内部的,节点移动时端口跟随节点变换,但连线路径计算却往往独立于端口模型进行。我们把它重新统一了一下,端口本身是独立的模型实体,挂在节点下,坐标是相对节点中心点的偏移量,这样变量就收敛了。连线的几何路径完全由两个端口的位置实时计算,渲染层只负责把路径画出来,这样一来,以后想支持端口随意拖动、动态增删,甚至节点旋转,都不会导致模型出现无法同步的问题。
1.3 渲染层为什么选择 SVG 而不是 Canvas
图编辑器对渲染层的选择,直接影响交互体验和开发成本。我们最终选型是 SVG,没选 Canvas,原因其实很直接:图编辑场景是典型的“少量元素高交互”模式,用户会反复点击、拖拽、悬停,Canvas 虽然性能上限高,但所有事件命中、区域重绘、图层管理都得自己实现,工程复杂度会陡增。而 SVG 是声明式的绘图模型,每个节点就是文档里的一个元素,天然支持事件命中、样式覆盖和局部刷新,对于两三千以内节点的图编辑场景,实测性能完全够用。
当然,如果节点规模上万,SVG 的 DOM 开销就会成为瓶颈,但我们通过数据层面做区域裁剪和分块渲染,也能在工程上缓解这个问题。这里面有个关键取舍:交互体验的优先级高于极限渲染性能。图编辑器不是图片浏览器,用户需要高频编辑,Canvas 的事件命中做得再好,也不如 SVG 的直接事件来得自然。所以选 SVG 是一种基于真实使用场景的工程判断,而不是技术信仰之争。
2. 数据模型与核心模块设计
2.1 节点、端口、连线的数据结构设计
diagram-design 的数据结构在设计之初就参考了很多成熟的图模型标准,但根据实际业务做了融合。一个节点包括 id、type、坐标(x、y)、宽度和高度、端口列表、业务数据字段,以及一些样式字段,比如颜色、图标、形状类型。端口列表是节点内部的一个数组,每个端口包含 id、名称、相对位置、方向,相对位置用数值表示,规则是坐标相对节点左上角的百分比,比如一个宽度 120 的节点,端口位置是 0.5、1.0,那就表示在节点底部正中。
连线的数据结构相对复杂,除了 id、源端口 ID、目标端口 ID 之外,还要存一个路由模式字段。路由模式一般有四种:直线、折线、正交线、曲线。直线适合节点距离近、层级简单的场景;折线适合流程图,能从视觉上区分方向;正交线适合架构图,上下左右都能走;曲线适合关系图,看起来柔和自然。DrawEdge这个核心函数会根据路由模式计算路径,每种模式对应一种路径生成算法。
以下是简化后的 JSON 示例:
{ "id": "canvas-001", "nodes": [ { "id": "node-a", "type": "service-node", "x": 120, "y": 80, "width": 160, "height": 60, "ports": [ { "id": "port-a-out", "x": 0.5, "y": 1.0, "direction": "bottom" }, { "id": "port-a-in", "x": 0.5, "y": 0.0, "direction": "top" } ], "data": { "name": "订单服务", "status": "running", "url": "/api/order" } } ], "edges": [ { "id": "edge-001", "source": "port-a-out", "target": "port-b-in", "route": "orthogonal", "data": { "protocol": "HTTP", "timeout": 3000 } } ] }2.2 连线的路由计算与避让策略
连线路由是图编辑器里最容易被低估的难点。如果你只是把源节点中心点和目标节点中心点连一条直线,那确实简单,但真实业务里,用户要求的是:连线不能穿过节点、连线的拐弯要清晰、多条连线不能叠成一坨。我们要实现可达的避让效果,主要分几步走。
第一步是判断是否需要避让。如果源端口和目标端口都在同一个方向上,且中间区域没有别的节点,可以直接走直线或正交折线。第二步是如果检测到连线路径经过其他节点的矩形范围,就自动把折线抬高或绕行,我们通过网格化的方式,把画布划分成若干格子,连线经过的格子如果被节点占用,就向相邻空闲格子绕行。第三步是有多条连线共享同一对端口时,给每条连线分配不同的弯折偏移量,避免完全重叠。
算法说起来难,但实际工程里反而要控制复杂度,因为用户对连线避让的容忍度其实很高,他们更在意的是“别穿过节点主体”和“看起来整齐”。我们实现了一个基于 AABB 碰撞检测的简化路由方案,性能开销小,视觉上也不差。极端复杂的避让需求交给了后续的人工手动调整连线拐点,这个功能也有做,就是允许用户拖动连线中间的点来微调路径。
2.3 画布坐标体系与缩放机制
图编辑器的坐标体系看起来简单,但它跟普通页面坐标之间有几层转换链路,处理不好就会出现拖拽时鼠标漂移的诡异现象。我们定义了一个根坐标系统叫做“世界坐标”,所有节点和端口存储的都是世界坐标,也就是说画布本身缩放、平移之后,这些数据不变。视口坐标则是渲染时通过一个变换矩阵映射得到的。
缩放机制上,我们把缩放中心点设置为鼠标位置。用户滚动滚轮时,先监听当前鼠标在视口中的位置,然后将这个位置转换到世界坐标中作为锚点,再调整 scale 值并计算新的平移偏移量。这样做的效果是,用户的鼠标始终指向同一个业务位置,视觉上非常自然。这里有一个很容易踩的坑:如果缩放中心用画布左上角,鼠标滚轮再滚也不会对准鼠标位置,很快就会让人感觉手感不对。
坐标转换的公式核心如下:
worldX = (viewportX - panX) / scale worldY = (viewportY - panY) / scale平移时只更新 panX 和 panY,缩放时先根据鼠标视口坐标算出新的 panX 和 panY。所有交互逻辑都必须基于世界坐标进行判断,渲染层再换算回视口坐标。
3. 核心交互与实操实现
3.1 拖拽节点与端口吸附
拖拽节点是图编辑器最基础的交互,但我们把它拆成了一套完整的状态机:按下鼠标、移动鼠标、抬起鼠标,每个阶段都有不同的行为。按下时先做一次命中检测,确认点中的是节点、端口还是空白区域;移动时实时更新节点的世界坐标,同时触发连线的路径重算;抬起时判断是否需要做吸附对齐。
节点吸附是细节之处的亮点。拖拽节点靠近另一个节点时,系统会比较两者的中心坐标差值,如果水平和垂直方向的差值都小于某个阈值(比如 6px),就自动把当前节点吸附到对齐位置。这个阈值不能太大,否则用户会觉得“自己很难精确摆放”,太小了又没效果。我们做了场景自适应,拖拽速度大于某个阈值时自动关闭吸附,防止快速拖拽时节点抽搐。
端口吸附逻辑跟节点吸附略有不同。当你拖拽一条新连线的终点靠近某个端口时,系统要先判断这个端口是否允许建立连接,比如输入端口不能连输入端口、端口协议必须匹配。符合条件的端口会高亮显示,并且连线的终点自动吸附到端口中心。这套机制的实现,依赖每次鼠标移动时对可见区域内的端口做一次候选集合筛选,数量控制在一百以内,性能不会有压力。
3.2 框选、多选与右键菜单
多选是图编辑器里提升效率的重要手段。我们的实现方式是:在画布空白区域按下鼠标并拖动,产生一个半透明的矩形选区,矩形与节点矩形相交的节点会被选中;如果按下 Shift 键,则选区是增量加到已选区集合上。选中多个节点后,用户可以直接整体拖拽、批量删除,也可以在右键菜单中对多选节点进行水平对齐或垂直分布。
右键菜单这块,我们没有用一个全局弹层,而是采用了“跟随上下文”的动态菜单组件。每次弹出前,根据当前选中的元素类型动态生成菜单项:选中的是节点时,菜单包含复制、粘贴、删除、设置样式;选中的是连线时,包含修改路由模式、删除连线、编辑业务数据;空白处右键菜单则包含粘贴、全选、撤销、重做、缩放适配。这样菜单项永远跟用户当前的操作上下文匹配,不会出现禁用态菜单项扎堆的尴尬。
框选性能的优化点在于:不要在鼠标每一次移动时都遍历全部节点,而是把上一次选区结果缓存下来,当节点位置不变时,只需要计算进入和离开选区边界的节点。但考虑到框选过程中节点自己是不会移动的,这个“位置不变”的前提其实是成立的,所以性能指标完全可控。
3.3 撤销重做机制的设计与实现
撤销重做做得好不好,直接决定编辑体验的底线。我们的方案并不是简单地把每一步操作都存一份全量快照,那样节点数量大的时候内存会爆炸。采用的策略是命令模式加反向操作记录:每一步操作都封装成一个命令对象,命令对象里保存了执行操作所需的前后状态,需要撤销时执行反向命令,需要重做时再次执行正向命令。
举个例子,删除一个节点这个命令里,保存的是被删除节点的完整 JSON 数据和它的位置索引。撤销时,在原来的位置重新插入节点,恢复它所有端口的连接关系;重做时,再次执行删除逻辑。对于拖拽节点这种连续高频操作,我们不能把鼠标每次移动都记成一步,否则撤销一次只能回退一像素。所以要把一整段拖拽过程合并成一个命令,按下鼠标时开始记录,抬起鼠标时才真正提交命令。
合并还有一个巧妙的点:如果在同一节点连续进行多次移动,可以将这些操作合并成一个“移动节点”命令实例,每次内存里只存上一个状态和最新状态,不产生重复记录。用户按撤销时,直接从最终坐标回退到最初坐标,非常干净。
3.4 属性面板联动与动态表单
图编辑器不仅仅是画布上的交互,左侧节点列表、右侧属性面板、底部快捷键工具栏、顶部缩放控制条,这些UI区域都要跟画布状态实时联动。属性面板是我们投入工作量比较大的模块,因为不同节点类型拥有完全不同的业务字段:服务节点有接口地址、超时时间字段;审批节点有审批人、审批策略字段;观众节点有昵称、连麦状态字段。
我们的方案是把属性面板设计成配置驱动的动态表单。每种节点类型对应一份字段配置,声明字段名称、类型、默认值、校验规则。当用户在画布上选中一个节点,属性面板根据节点类型动态渲染一份表单,用户修改表单的任意字段,都会实时更新画布节点里的数据和样式。这里同样引入了命令模式,每次修改属性也会生成一条可撤销命令,保证用户修改属性后还能正常撤销。
属性面板跟画布联动时有个注意点:不能每次输入框变化都触发全量渲染,否则输入中文会出现光标跳动。解决方法是把属性更新的写入频率做节流,输入框聚焦时本地维护一份临时值,失焦时才提交到图模型并触发节点重绘。这个细节处理不好,图编辑器会被吐槽“连属性都填不了”。
4. 渲染效率优化与实战性能调优
4.1 局部渲染与脏矩形的原理
图编辑器的性能优化不能靠一句“用 Canvas 更快”解决,而是要针对 SVG 的特性做局部更新。我们的渲染层维护了一个“脏矩形”列表,当节点位置、大小或样式发生变化时,只把这个节点所在的矩形区域标记为待重绘。SvgCanvas 层的核心流程是:每次操作行为触发模型更新后,把涉及的节点、连线、端口列表收集起来,再统一执行属性更新,而不是整体清空重建 SVG。
对于节点数量在几百到几千之间的场景,局部更新策略收益非常明显。比如拖动一个节点时,真正需要重绘的是这个节点本身和所有跟它相连的连线,数量往往在几十条以内,每帧的几何计算量也就几千次浮点运算,浏览器完全能承受。我们实测过,开启局部更新后,3000 个节点的画布上拖动单个节点,帧率稳定在 55fps 以上,而未优化前整体重绘只有十几帧,体验差距巨大。
还有一个容易忽视的性能杀手是样式重排。SVG 元素一旦频繁修改 transform、x、y 属性,浏览器会触发大量的布局计算。我们优化时把节点的坐标尽量合并在一个 transform 里,利用矩阵变换同时表示平移和缩放,而不是直接改 x / y 属性,这样能明显减少重新布局的频率,滚动缩放也更顺滑。
4.2 连线数量多时的渲染优化
图数据里最容易被忽略的是连线的渲染成本。一条连线包括路径元素、箭头标记、交互热区,展开后可能就是三个 SVG 元素。当一个节点连接几十条连线时,SVG 的 DOM 节点数量会成倍增长,而且路径计算本身也有成本。我们对连线做了分优先级渲染:选中的连线、鼠标悬停的连线、跟当前节点相关的连线条设置高优先级,在重绘时优先更新;其余连线只在锚点发生位移时才重算路径。
连线交互热区的实现也做了优化。因为 SVG 的 stroke 宽度如果设置得很小,用户很难点中;设置得过大,多条连线叠在一起又会互相遮挡。我们的做法是:连线路径本身用 2px 的不可见热区路径和 1px 的可见路径共同组成,热区宽度设为 8px。事件绑定只在热区路径上,这样既保证点击命中率高,又不会挡住其他元素的操作。
当连线数量达到五千条以上时,SVG 的 DOM 数量还是会逼近极限。我们在这个阶段会开启“简化模式”:把距离视口较远区域内的连线隐藏掉,只保留节点占位。虽然业务场景里用到五千条连线的情况不多,但这个兜底方案在数据大屏场景里很有效,实测渲染节点数和首屏时间都能控制在一个合理的范围内。
4.3 大数据量节点的虚拟滚动策略
节点数量超过两三千时,可以思考是否所有节点都必须真实渲染到 SVG 中。我们参考虚拟列表的思想设计:只渲染视口内可见的节点,视口外的节点在模型中保持数据,但不在 SVG 中创建元素。当用户平移画布或缩放时,动态计算当前视口对应的世界坐标范围,找出相交的节点列表,进行增量挂载和卸载。
这个策略实现起来并不复杂,但能带来质变。画布平移后,旧节点离开视口范围,立即从 SVG 上下文移除,新进入视口的节点才创建。这个“创建-销毁”的过程频繁发生,会带来额外的 GC 成本,所以我们做了节点池复用,创建过的 SVG 元素缓存在内存里,进入视口时重复利用,而不是每次全新建立,实测内存增长非常有限。
节点复用需要注意一个问题:节点对象里如果绑定了自定义事件监听,复用的时候容易累积多个监听器导致内存泄漏。我们的做法是事件绑定委托到画布容器层,利用事件冒泡机制统一处理,这样每个节点元素自身不需要绑定监听器,复用时没有任何清理负担。这也是做图编辑器初期就应该打好的基础。
5. 常见问题与排查技巧实录
5.1 坐标偏移与拖拽漂移
图编辑器最经典的诡异现象:拖拽一个节点时,鼠标越动越快,节点位置总差一段距离。这通常是因为转换坐标时忘记减去画布容器的偏移量,或者监听的坐标系混用了 clientX 和 offsetX。排查方式很简单:在鼠标移动的 handler 里打印原始事件坐标,再打印经过转换后的世界坐标,跟节点当前位置做对比,看到底是哪一层转换断了。
还有一种情况是缩放时坐标漂移。放大后发现鼠标指到哪里,节点就偏到哪里。原因是缩放时没有实时更新 pan 值,导致世界坐标和视口坐标的比例关系失效。修复时需要同时维护 pan、scale、以及缩放锚点这三个变量,任何一个更新顺序错了,都会造成累计误差。我常用的调试技巧是:在缩放操作结束后,把鼠标当前视口坐标乘以 scale 加上 pan,看看得到的值是否稳定等于缩放前的世界坐标,如果差值很大,说明锚点计算有误。
5.2 连线命中困难与误触
用户反馈点连线点不中,或者点一个节点的时候总是触发旁边连线的事件。这类问题九成以上是热区路径的宽度不够,以及事件绑定层级不对。检查一下热区路径是否真的覆盖到了手指或鼠标的点击区域,同时确认连线元素上有没有被其他兄弟元素遮挡。我们遇到过一种情况是高亮框和连线在同一层级,点击时命中的是上面的高亮透明框,导致永远触发不了连线事件,解决办法是把连线的 z 序提到最高层级,并且只在热区绑事件。
误触的另一个来源是双击与单击难以区分。用户想双击修改节点文本,结果每次单击都触发连线拖拽。我们的处理是区分单击、双击和拖拽三种状态:按下和抬起之间的位移小于 5px 且时间小于 200ms,判定为单击;在同一位置快速出现两次单击判定为双击;位移超过阈值则取消单击状态,转成拖拽。这个判定逻辑写在画布层,全局统一处理,不会因为某个具体元素而行为不一致。
5.3 撤销重做后状态丢失
撤销重做做得不够好,最容易暴露的问题是节点被删了之后,撤销回来连线和外部数据全丢了。原因是删除的时候只记录了节点自身的快照,没有记录跟节点相关的连线信息。我们的修复方案是:在删除命令里不仅保存被删节点,还要遍历所有连线,把源端口或目标端口指向该节点的连线全部收集起来,连同节点 JSON 一起保存。撤销时先恢复节点,再重建连线并重新挂接端口。
还有一类状态丢失,发生在属性面板的联动数据上。用户修改了节点属性,撤销后发现画布更新了但属性面板还是新值。这是因为属性面板做了本地临时值缓存,没有跟命令日志关联。解决方法是每次命令提交后,统一向外抛出一个变更事件,面板监听到事件后重新从模型拉取当前值,而不是用本地缓存。这样代码逻辑更统一,也减少了状态同步的遗漏点。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 拖拽节点时鼠标漂移 | 坐标转换未考虑画布容器偏移量 | 统一在画布层做视口坐标到世界坐标的换算 |
| 缩放时光标指向位置跳动 | 缩放锚点计算错误 | 以鼠标世界坐标为锚点,实时更新 pan |
| 点不中连线 | 热区宽度太小或层级被遮挡 | 热区宽度设为 8px 以上,连线 z 序提高 |
| 节点拖动后后端渲染错位 | 世界坐标与百分比端口位置混用 | 端口位置用相对百分比,节点坐标用世界绝对坐标 |
| 撤销后连线丢失 | 删除命令未记录相关连线 | 删除节点时同时收集相关边并保存快照 |
| 输入节点名称时光标跳走 | 每次输入都触发全量重绘 | 输入框本地缓存值,失焦才提交模型并触发重绘 |
| 画布加载大量节点卡顿 | 视口外节点也被渲染 | 开启虚拟滚动和区域裁剪,复用节点 DOM |
6. 工程化落地与后续扩展思路
6.1 组件化封装与对外 API 设计
diagram-design 最终不是一个只在项目里自嗨的工具,而是要封装成可复用组件,供不同业务方接入。我们在工程化上把它拆成了三层:核心层负责数据模型和命令日志,纯 JavaScript 逻辑,不依赖任何渲染框架;渲染层基于现有前端框架做了适配器,对外暴露画布容器和事件回调;业务层则提供属性面板、缩放控制器、工具栏等开箱即用的 UI 组件。
对外 API 设计上,重点不是提供方法,而是提供事件流。业务方在初始化时给定一份图数据配置和渲染回调,之后所有图数据变更都通过内部消息系统广播出去,业务方可以通过订阅事件来感知节点新增、删除、移动、连线建立等变化。这种设计比直接暴露一堆 setter 方法更安全、更符合单向数据流的约束,也让未来接入像 D3 或 G6 这样的扩展库时不会破坏原有状态。
封装组件时有个容易犯的错误:把所有内部状态都暴露出去,导致业务方随意修改状态,引发渲染和模型不同步。我们的做法是只暴露几个核心方法:setData用来整体载入图数据,getData用来导出图数据,addEventListener用来订阅事件,undo和redo用来触发撤销重做。其他内部状态全部私有化,业务方无感知地使用,让整个组件保持黑盒,降低集成成本。
6.2 插件机制与主题定制
做通用图编辑器,插件机制是绕不开的。我们可以参考很多成熟编辑器的插件习惯,把能力边界清晰地暴露给外部,同时限制业务方对核心的破坏。diagram-design 的插件机制分为两类:一类是渲染插件,负责自定义节点类型,业务方可以注册一个节点类型名及其对应的渲染组件函数,渲染层在遇到这类节点时自动调用注册的组件进行渲染;另一类是行为插件,负责注册新的交互行为,比如一段自定义快捷键逻辑,或者一个批量布局入口。
主题定制的实现跟插件机制不同,它是一条独立的配置通道。我们定义了一套与业务无关的主题 token,包括画布底色、节点主色、连线颜色、选中态颜色、边框颜色、阴影强度等。业务方接入时传入一份 theme 对象,渲染层在初始化时把 token 转换成 CSS 变量,所有节点和连线都引用这些变量,这样切换主题时不需要重绘任何图形,只需要更新样式变量,页面会自然响应变化。这个方案简单实用,比挨个改 SVG 属性高效得多。
6.3 多场景复用经验与展望
diagram-design 做到现在,已经不止服务于原来的流程编排场景,后续还有推进一些新的场景:某个团队把它用在了组织架构图上,节点样式换成了头像卡片,连线换成了汇报关系;另一个团队需要做服务网格的可视化配置,在节点里嵌入了健康状态灯和流量指标,直接复用基础模型,只扩展业务插件即可。事实证明,只要数据模型保持领域中立,渲染层做好扩展点,一套图编辑基础设施是完全可以横跨多个业务域的。
后面我们还有一些值得实践的方向,比如把非可视化的领域数据自动布局成美观的图结构;再比如把图编辑器的交互能力与 AI 生成流程打通,让用户输入一段自然语言需求,系统自动生成初始图,再由用户自由编辑微调。这类想法听起来很遥远,但底层依赖的还是今天打好底子的图模型、渲染性能和插件机制。这也是为什么建议不要轻易走向看似简单的套壳方案,一旦数据模型和交互逻辑被锁死,后面每一步创新都会很难受。
写在最后
最近有人问我,做 diagram-design 这种项目最值得的收获是什么。我认真想了一下,其实不是那一堆渲染算法和性能调优技巧,而是真正体会到了“数据模型是一切的根”这句话的分量。图编辑器好不好用、能不能扩展,百分之八十在数据结构设计阶段就已经决定了。节点、端口、连线、命令日志,这些抽象做好后,SVG 代码、拖拽交互、属性绑定其实都是非常自然的编织过程。如果你也想做类似的东西,建议第一步先别急着写代码,把自己的领域对象梳理清楚,把数据模型画出来,再开始动手。我踩过的那些坑,很多都是序不对的锅,这里一并写出来,希望你能绕开。