资讯动态

从零构建企业级图编辑系统:数据建模、渲染引擎与协作实践

发布时间:2026/10/21 19:15:06 来源:尧图企业网站定制
最近在负责一个内部代号为diagram-design的图表设计系统前后折腾了几个月从最初几个人随手画架构图、流程图到后来形成一套能支撑多个业务模块复用的可视化基础设施中间踩了不少坑也沉淀了一些比较扎实的结论。这篇文章想把整个项目的核心逻辑、选型思路和实操细节完整复盘一遍供准备做类似“图编辑/可视化设计器”的同学参考——不管你是前端、全栈还是后端想接入可视化能力的人应该都能从这里拿走一些可直接落地的经验。先交代背景我所在的团队需要同时维护好几类“图”——业务流程图、系统架构图、网络拓扑图甚至部分时序说明图。以前这些图分散在各个文档里用不同的工具画格式不统一、交互不一致更麻烦的是“画完就死”没有任何可复用的数据结构和扩展能力。diagram-design 的定位很朴素做一个统一的可视化编辑系统能画、能存、能导出、能嵌入到不同的业务页面里去。但越做越发现一个“看上去只是画图”的项目牵扯到的难点一点都不少数据模型怎么设计、渲染引擎怎么选、交互怎么做得跟手、视觉怎么保持一致、数据更新怎么不卡顿、多人协作要怎么起步。这些问题的答案光靠调用几个现成的图画库是远远不够的。下面按我从零搭建这个系统的实际顺序来讲每一部分都会把“为什么这么设计”讲透也会把实际操作中容易翻车的地方拎出来。1. 项目初期最难的不是选技术栈而是定义“要画哪些图”这个项目最开始踩的第一个坑就是团队里每个人对“我们想要什么”的理解都不一样有人想要一个能把代码依赖关系自动可视化的工具有人想要一个支持手绘风格流程图的画板还有人想做类似白板一样的自由绘图页。如果一开始就照着所有人的需求去堆功能这个项目大概率会在第一个月就烂尾。1.1 先把图的类型拆清楚一张图不是另一张图我在项目启动的第一周做了一件事把所有潜在需求汇总按“图的本质结构”分成几类。这个分类决定了后续的数据模型和渲染策略非常重要。对这几类图来说底层数据结构其实是统一的一张图 一组节点 一组边。差别主要在节点的语义、边的含义、以及布局的约束。图类型典型节点典型边布局特点核心难点流程图步骤、判断、开始/结束流转顺序从上到下或从左到右分支收敛分支多时排布容易乱架构图服务、数据库、网关依赖、调用分层、按区域聚簇层级关系和分组关系同时存在拓扑图设备、主机、机房链路、连接可自由摆放强调空间关系节点常多达数百上千时序图参与者、消息调用时序严格按时间轴排列对“顺序”的感知要求极高做完这个分类之后很多模糊的需求就清晰了依赖自动可视化本质上是要做一种“从代码库解析出图数据然后套用架构图布局”的功能而不是去发明一种新的图手绘风格则纯粹是视觉主题问题跟数据结构无关。1.2 该砍掉的需求一定要砍掉做图编辑器的宿命明确“要画哪些图”之后紧接着要明确“不画哪些图”。这一步做不好项目的范围会无限膨胀。我当时坚持砍掉的包括不做数据图表柱状图、饼图、折线图这类图本质是“数据到视觉编码的映射”和 graph 结构完全不同直接用成熟图表库就行不需要自研编辑能力。不做完整 UML 建模图UML 图的规则太多继承、关联、聚合、组合、多重性完全照规范做会让数据模型复杂度翻倍。内部使用场景只要“类框 连线”就够真要严谨建模可以用专业建模工具。不做無边界实时白板第一版不做无限画布、无限缩放、多人并发画任意图形……这种场景的复杂度远超预期第一版老老实实做“有限画布 网格”等基础稳定了再考虑扩展画布范围。有个印象很深的例子当时某位同事提需求说“要能把整个系统的架构画到一张大图上”听起来很合理但他那个系统有一百多个服务。如果真做成一张图画出来又密又乱滚动都卡根本没法用。我的处理方式是给他拆成“视图”——按业务域或分层关系拆成多张中小型图并支持跨图跳转。这个妥协带来的体验提升是显著的也说明了一个道理图编辑器的限制有时候反而是它可用性的保障。2. 数据模型是地基业务数据和视觉坐标绝不能混在一起画图功能看起来简单一拖一放就完了但真正决定这个系统能走多远的是最底层那套数据结构设计。diagram-design 的地基就是一件事把图的数据模型和视觉模型彻底分离。2.1 一张图在内存里到底是什么实现层面一张图最终会落成类似这样的结构interface GraphDocument { version: 1.0; id: string; nodes: GraphNode[]; edges: GraphEdge[]; } interface GraphNode { id: string; // 全局唯一且稳定不因拖拽而改变 type: string; // 节点类型service | step | device ... x: number; // 视觉坐标由图形引擎维护 y: number; meta: Recordstring, unknown; // 业务数据服务名、状态、告警级别…… } interface GraphEdge { id: string; source: string; // source 节点 id target: string; // target 节点 id style?: EdgeStyle; // 可选虚线、颜色、粗细等视觉属性 meta: Recordstring, unknown; // 业务数据链路带宽、调用次数…… }这里最容易犯的错就是把业务数据和视觉数据揉在一起。比如直接在meta里存一个position字段或者反过来把节点的颜色、图标、尺寸全都变成业务逻辑的依赖项。两种写法在前期都很快但后面做多种视图切换、多人协作时会非常痛苦。2.2 坐标为什么是“视觉态”而不是“业务态”用一个具体场景说明用户的某张架构图里有“数据库”节点在“部署视图”里它应该排在某个位置在“监控视图”里它也应该排在那个位置——两个视图的逻辑位置是同一个坐标没问题但假如你有一套自动布局在“部署视图”下把所有节点按层次重新排了一遍这时候如果你把坐标写死在业务字段里就会出现布局引擎和用户手动拖拽打架的混乱局面。我的做法是坐标是 ViewModel 层维护的视觉缓存。业务层只关心“有哪些节点、哪些边、节点属于什么类型”图形渲染层才关心“这个节点当前画在哪个位置”。这样带来了几个明显好处切换布局算法时只需要重新计算并写入 ViewModel 的坐标业务数据完全不动撤销/重做只需要记录 ViewModel 上的操作不需要深拷贝整个业务数据多人协同的时候业务字段的冲突远比坐标冲突重要把坐标单独分开能有效降低冲突率。2.3 数据层、模型层、渲染层、交互层的职责划分工程上我把系统分成了四层每一层的职责在代码里也做了强制隔离层级职责典型实现不允许做的事数据层增删改查节点/边业务元数据管理GraphStore不直接操作画布模型层维护坐标、视口、选中态、布局结果ViewModel/Viewport不保存业务字段渲染层把节点/边绘制到 Canvas/SVGRenderer不直接修改数据交互层拖拽、框选、连线、缩放平移InteractionController不绕开模型层直接改数据这套分层的核心价值体现在一个场景上节点做了 hover 高亮。交互层收到pointermove让模型层标记“节点 A 处于 hover 态”渲染层只在下一帧绘制 A 的高亮效果没有哪个地方会去直接改GraphNode的某个字段。这样既利于调试也让后续做单元测试变得非常顺。3. 渲染引擎选型Canvas 为主、SVG 为辅、WebGL 暂不碰这是 diagram-design 里决策最重的一环因为渲染引擎选错了后面所有交互和视觉都会跟着遭殃。我当时做了约一周的调研和 Demo 对比最终方案是主画布用 Canvas 2D少量迭加元素用 SVG不引入 WebGL至少现阶段不碰。3.1 为什么不是 SVG 也不是 WebGL我做了个 500 节点 1000 边左右的压力 Demo在普通笔记本上跑SVG 方案在节点少于 200 时体验尚可一旦节点数涨到 500拖拽和整体平移会出现明显掉帧。原因不复杂——每个节点都是一个真实 DOM 节点加上每条边的 pathDOM 数量很容易破千浏览器在维护布局和事件绑定上都会变慢。Canvas 方案在同规模下流畅度好得多。它本身就是像素绘制没有 DOM 扩容问题事件可以通过坐标命中检测自己实现反而更可控。WebGL 的性能当然更强但文本排版、高 DPI 适配、复杂交互拾取都要自己造轮子对于“图编辑”这种交互密集型场景WebGL 的投入产出比很低。对比下来结论很清晰对“图编辑”这种交互密集、元素数量在几百到几千的场景Canvas 2D 是性价比最均衡的选择。SVG 则作为辅助层使用——拖拽时的瞄准线、hover 高亮框、临时绘制的橡皮筋矩形这类零散元素交给 SVG省去手动处理它们与业务元素的遮挡关系。3.2 Canvas 最烦的问题命中检测必须自己做DOM 天然支持elementFromPoint但 Canvas 是一张“平铺像素”的画布你想知道鼠标点到了哪个节点有两条路几何运算遍历所有节点做矩形/圆形命中测试。节点几百个时性能没问题上千个时也能接受但不同图形形状要做不同的命中规则实现起来繁琐。离屏 Canvas 拾取在每帧渲染时额外生成一张与可见画布等大的“颜色索引图”每个节点用唯一颜色填充鼠标移动时读取该像素的颜色反查映射表就能瞬间得到节点 ID。颜色精度用 24 位色就能支持上百万节点完全够用。我最终采用的是“颜色索引离屏 Canvas 几何运算兜底”的组合默认用颜色索引快速定位节点当渲染层标记某个节点不可拾取或不参与命中时再走几何运算。伪代码大致是这样的function pickNodeAt(screenX: number, screenY: number): string | null { const pickCanvas renderer.getPickerCanvas(); const pixel pickCanvas.getContext(2d).getImageData(screenX, screenY, 1, 1).data; const colorKey pixelsToColorKey(pixel); // 例如 #a1b2c3 return colorIndexMap.get(colorKey) ?? null; }这个方案还有一个额外的好处拾取逻辑与渲染逻辑解耦。哪怕你自定义了带透明区域的不规则节点图形只要绘制索引层时也按同样规则填色命中检测就能保持完整。3.3 Canvas 文本排版是个隐藏大坑节点内部通常要显示图标和文字。Canvas 的文字排版能力很弱没有自动换行、自适应宽度、垂直居中这些现成能力。我的做法是封装了一个简单的文本测量工具function fitTextToWidth(text: string, maxWidth: number, fontSize: number): string { if (measureText(text, fontSize).width maxWidth) return text; // 二分截断 省略号或者按字符串长度逐字递减 let low 1, high text.length; while (low high) { const mid Math.ceil((low high) / 2); if (measureText(text.slice(0, mid) …, fontSize).width maxWidth) low mid; else high mid - 1; } return text.slice(0, low) …; }中英文混排、字体回退没有指定字体时中文和英文可能用不同字体渲染、高分屏下的清晰度这些都要单独处理。做一个能看能用的绘图引擎不难但要把 Canvas 画出来的文字和 DOM 排版一样精致工作量远超预期。4. 交互层设计跟手、准确、不打架图编辑器最核心的体验就是“能不能流畅地拖、准确地连、自然地缩放”。这部分我把鼠标和触控板的行为统一处理走的是指针事件Pointer Events好处是一套代码同时覆盖鼠标、触摸笔和触屏。4.1 视口变换屏幕坐标与画布坐标的换算所有交互都绕不开坐标转换。我的 Viewport 模型很朴素保存{ x: number; y: number; scale: number }表示“画布坐标原点在屏幕上的偏移”和“缩放倍数”。屏幕坐标转画布坐标的公式就是function screenToCanvas(screenX: number, screenY: number, viewport: Viewport) { return { x: (screenX - viewport.x) / viewport.scale, y: (screenY - viewport.y) / viewport.scale, }; }平移操作监听pointerdown时如果命中空白区域进入“平移模式”之后每次pointermove都累加偏移量到viewport.x/y渲染层重绘。缩放操作以鼠标位置为缩放中心等比换算保证鼠标下的那个画布点在缩放前后屏幕位置不变否则滚轮缩放会“越来越飘”。4.2 拖拽节点的三个细节拖拽节点看着简单里面藏着三个容易忽略的点第一个是“先拾取再拖拽”。必须在pointerdown时先完成命中检测确认这个坐标命中了哪个节点并且该节点的类型允许被拖拽才能进入拖拽状态否则直接进入平移模式。如果不加区分用户想拖节点结果把整个画布拖走了体验会非常差。第二个是“吸附网格”。我实现了网格对齐把画布坐标取整到最近的网格点默认 8px缩放级别变化时网格间距是固定的画布单位 8所以换算成屏空间的时候需要grid / viewport.scale作为判定阈值。function snapToGrid(value: number, gridSize: number): number { return Math.round(value / gridSize) * gridSize; }第三个是“拖拽状态要有唯一归属”。拖拽期间必须明确当前只有这一个节点被拖动任何 hover 效果、框选操作、连线操作全部让位避免交互事件互相打架。4.3 连线从锚点到贝塞尔的完整链路连线交互对用户感知的影响很大。我的实现是这样的每个节点可以提供若干“锚点”连接端口鼠标 hover 到锚点上时变化高亮按下锚点后进入“连线模式”渲染一条从起点锚点到鼠标当前位置的临时贝塞尔曲线实时跟随鼠标松开鼠标时判断是否落在目标节点的锚点区域内若是则创建一条正式边否则取消连线。临时连线的绘制要保证视觉层级始终在普通元素之上并且不污染业务数据——它只存在于交互状态里不写入 GraphDocument。这里有一个实测经验临时曲线的控制点要根据两个节点的相对位置动态计算否则有些情况下曲线会画出很奇怪的折角。最简单可靠的方案是默认用水平方向的贝塞尔起点在右侧、终点在左侧时控制点取source.x distance/2看起来像左到右的流。4.4 框选和命中检测的取舍框选橡皮筋选择我采用的是“遍历节点 包围盒相交”的几何方案没有走颜色拾取因为框选需要同时算出“哪些节点在矩形内”逻辑上是范围计算几何遍历更直观function nodesInRect(nodes: GraphNode[], rect: Rect): GraphNode[] { return nodes.filter(node { return node.x rect.x node.x rect.x rect.width node.y rect.y node.y rect.y rect.height; }); }节点数到上千时这个遍历的效率仍可接受每帧一次过滤的开销约 1-2ms到上万时再考虑引入四叉树索引不值得为不可能出现的规模提前优化。但要注意框选矩形是用屏幕坐标定义的必须先用screenToCanvas转成画布坐标再做计算否则平移缩放后框选区域会错位。交互层另一个核心经验是给交互流程做状态机。我的交互控制器里维护了idle | hovering | draggingNode | connecting | panning | lassoSelecting等状态每个状态只处理自己关心的事件type InteractionState | { type: idle } | { type: hoveringNode; nodeId: string } | { type: draggingNode; nodeId: string; offsetX: number; offsetY: number } | { type: connecting; sourceNodeId: string; sourceAnchor: AnchorKey } | { type: panning } | { type: lassoSelecting; startX: number; startY: number };这个状态机让不同交互之间不会互相干扰也避免了“每个功能各自监听事件”导致的全局事件风暴。做图编辑器的人经常忽视这一点结果就是代码越写越乱各种鼠标事件处理器在全局互相污染。5. 视觉一致性与主题系统让每一张图看起来“像一个体系”想要支撑多种业务场景视觉上必须有一致性。一开始大家画出来的图五花八门后来我引入了一套设计 Token 体系本质是把颜色、间距、字号、圆角、线宽等视觉要素统一成变量再按主题包切换。5.1 设计 Token从“感觉”变成“规则”拿节点样式举例我定义了这么一组 Tokeninterface ThemeToken { name: string; canvasBackground: string; node: { fill: string; border: string; borderHover: string; borderSelected: string; radius: number; shadowBlur: number; paddingX: number; paddingY: number; fontSize: number; fontColor: string; }; edge: { color: string; colorHighlight: string; width: number; arrowSize: number; dashLength: number; dashGap: number; }; anchor: { fill: string; size: number; activeFill: string; }; grid: { color: string; size: number; }; }有了 Token新增一个节点类型或者新主题时只需要改配置不需要跑到渲染函数里去挑颜色。这是“视觉一致性从结果把控到源头把控”的一步。5.2 缩放时的三个视觉感官问题Canvas 渲染在高分屏和缩放时有一些很典型的视觉瑕疵这三个问题我花了不少时间才彻底解决高分屏模糊Canvas 用物理像素绘制时在高 DPI 屏幕上会发虚。解决办法是在初始化时根据devicePixelRatio把画布缩放上去并把逻辑坐标和物理坐标做映射。线宽被缩放如果直接用画布坐标线宽如ctx.lineWidth 1缩放 0.5 倍时线条会变成一半宽半透明看起来断断续续。我的做法是让线宽始终对应一个最小屏幕尺寸在渲染前乘以1 / viewport.scale保证图形边界清晰。文字在缩小时糊成一团缩放小于某个阈值比如 0.5时节点文字直接重画为一个简化的“占位块”鼠标悬停或放大到阈值以上再显示完整文字。这个“LODLevel of Detail”策略在大图缩小时对性能帮助也很大。5.3 暗色模式的适配不是“换个背景色”暗色模式如果只是把底色变黑、文字变白效果通常很差。我采用的是“主题包 反转映射”思路所有颜色从 Token 里取暗色主题里主色、中性色、功能色分别重新定义而不是简单给亮度取反。节点阴影在暗色底上要更收敛边框明度适当提高否则节点边缘会“隐形”。主题切换在运行时重绘所有节点和边即可坐标和业务数据完全不受影响。这一步再次体现了“数据/模型/渲染分离”的价值。5.4 动画参数的克制好的交互动效是平滑、克制的坏的动效是每个都加、互相冲突。我的经验是hover 高亮120ms 过渡不能太长否则不跟手节点选中150ms 加一圈外发光同时放大 1.02 倍有“拾起”感自动布局动画400ms 缓动让节点从旧位置移动到新位置更符合“布局更新”的语义拖拽移动不设缓动直接跟随鼠标否则会有“滞后感”。动画参数全部放到 Token 里不接受在渲染函数里写死。我在项目里测试过很多参数目前的这套组合在流畅感和反馈明确度之间比较均衡。6. 增量渲染与布局策略大数据量下不卡顿的关键支持几百个节点和上千条边之后最影响体验的就是“数据更新后整个画布的重绘”。最开始我图省事所有数据变更都触发一次全量重绘到了 500 节点 1000 边时一次全量重绘耗时约 30-40ms交互拖拽时帧率掉到 30fps 以下肉眼能感觉到明显卡顿。6.1 从“全量重绘”到“脏区重绘”Dashboard 场景是典型例子监控图每隔几秒刷新一次节点状态刷新时如果每次全量重建整个画布会闪用户根本没法盯。优化思路很朴素只有标记为 dirty 的节点和与它相连的边才需要重绘。renderer.markNodeDirty(nodeId); renderer.markEdgeDirty(edgeId); // 在 requestAnimationFrame 回调里只重绘画布上这些脏元素所在区域Canvas 本身没有“局部重绘”接口但可以通过只裁剪脏区来进行局部清理和重绘。实现细节是维护一个脏矩形集合每次渲染时用ctx.save()ctx.clip()限定区域重绘该矩形内的元素。这样 500 节点场景下一次局部状态刷新的重绘时间能从几十 ms 降到 1-2ms肉眼体验是零卡顿。6.2 帧预算一次 raf 处理不完就分帧重绘的频率由requestAnimationFrame驱动我给它加了“帧预算”机制每帧留给渲染的时间预算是 16ms超过预算的部分延迟到下一帧处理。这套机制本质上是把重绘任务排成队列避免某一帧因为脏元素过多而卡很长时间。对交互密集型编辑器来说保持稳定的帧间隔比追求单帧高吞吐更重要。6.3 布局引擎的“一次生成 手动优先”策略图编辑器跑自动布局是很多人的期望拖拽后自动排整齐。全自动布局每次数据变更都重新布局在拖拽场景下会打架——用户刚把节点拖到某个位置自动布局立刻把节点排走体验极其崩溃。我的策略是用一套布局引擎内部用 dagre 风格分层布局思路跑“初始布局”只在以下两种情况触发布局新建图文档时自动布局一次生成合理初始位置用户显式点击“自动布局”按钮时重新计算所有节点位置并播放移动动画。其余任何时刻用户手动拖拽过的节点坐标都是最高优先级。布局结果算的是“建议坐标”写入的是新的一版 ViewModel不影响业务数据用户手动动过的节点位置则记录在 ViewModel 的“锁定节点集合”里布局引擎不再参与计算。这个策略同时兼顾了“想要自动布局的便捷”和“手动操控的自由”。6.4 实测数据与后续优化方向我拿一个 500 节点 1100 边、节点都带一定业务元数据的图做过压测操作全量重绘耗时脏区重绘耗时表现单节点状态刷新35ms1.8ms全量方案掉帧增量方案流畅拖拽单节点32ms/frame8ms/frame含连线拖动拖拽跟手无明显掉帧全图缩放平移38ms/frame-缩放时仍有一点卡后续考虑 LOD 优化自动布局500节点-约 200ms布局计算一次性完成动画播放可接受后续如果节点数再往上翻一个量级我可能会把布局计算丢进 Web Worker主线程只管渲染和交互拾取层也换成二分空间索引。不过就当前的业务规模来说这套方案已经足够“稳”性能预算也不用拉满。7. 协作与扩展从单人画图到团队一起维护一张图diagram-design 做了一段时间后使用范围从“画完导出图片”变成了“多人实时维护同一张架构图”。这带来两个新问题多人同时编辑怎么同步、新节点类型怎么低成本接入。7.1 以 JSON 为协议、以原子操作为消息多人协作不能每次动一下就把整张图 JSON 全量同步一遍数据量大时网络负担太重而且会产生大量无用冲突。我的做法是定义一组原子操作消息type GraphOp | { type: addNode; payload: GraphNode; timestamp: number } | { type: removeNode; id: string; timestamp: number } | { type: moveNode; id: string; x: number; y: number; timestamp: number } | { type: addEdge; payload: GraphEdge; timestamp: number } | { type: removeEdge; id: string; timestamp: number } | { type: updateMeta; id: string; diff: Recordstring, unknown; timestamp: number };本地先立即应用操作再通过 WebSocket 广播出去远端收到后同样应用。服务端只负责转发不做业务判断。这套“本地乐观更新 原子操作广播”的方案虽然不解决所有冲突多个用户同时拖动同一个节点的坐标会有 last-write-wins也就是后写入的覆盖先写入的冲突语义见后续——对图编辑场景来说坐标覆盖并不致命反而比文本编辑那种复杂的合并规则容易落地得多。7.2 撤销/重做的正确打开方式我第一版做撤销时偷懒每操作一次就把整个 Graph JSON 快照压进栈里。结果嘛——用户多操作几次内存暴涨撤销也慢而且因为快照里带有渲染层的坐标状态撤销操作经常把用户刚刚微调过的布局也一并“回滚”掉了很恼人。正确办法是让撤销/重做栈只记录GraphOp并且通过“逆操作”来实现撤销原操作撤销操作addNoderemoveNode同 idmoveNode恢复上一个坐标addEdgeremoveEdge同 idupdateMeta恢复上一个 meta 片段因为整套系统都是围绕 GraphDocument ViewModel 构建的所有操作天然落成“可逆操作”。这里对“坐标是视觉态”的好处体会特别深——撤销 layout 的某次操作时只需要更新 ViewModel 里的坐标不需要碰业务数据。7.3 节点类型的插件化不能让内核承载所有业务形态业务方总是会提出各种新节点需求比如“这个节点要支持状态图标”“那种节点要能展开/收起子节点”。如果每种新需求都改内核系统会迅速腐化。我设计了节点注册表机制把“绘制、命中、测量、配置面板”四个能力统一交给节点类型class NodeRegistry { private static types new Mapstring, NodeRenderer(); static register(type: string, renderer: NodeRenderer) { NodeRegistry.types.set(type, renderer); } static get(type: string): NodeRenderer | undefined { return NodeRegistry.types.get(type); } }内置节点类型比如通用方块、图标节点、分组节点全部走注册表注册新的业务节点类型只要实现这套接口就能接入不需要改动渲染层、交互层和数据层。这里体会到“注册表模式”在可视化系统里的价值比想象中大得多——它把“系统的能力”和“业务的需求”之间的耦合降到了最低。7.4 多人协作第一版不必上 CRDT/OT很多团队一听说多人协作就直接上 CRDT无冲突复制数据类型这类重武器。但我们的场景是几百个节点的图编辑不是极端并发的高密度文本协作第一版采用“原子操作广播 last-write-wins”的组合完全够用实现成本低很多稳定性也好控制。按时间戳覆盖的方式编辑器里偶尔会出现“这个节点的位置被对方动了一下”但从产品角度看可接受后续如果需要更精细的合并再逐步升级。结尾如果让我重新做一次我会改掉这三个决定这个项目做到现在整体方向没有跑偏但如果现在让我重新从零开始有三件事我一定会一开始就做对而不是中途再返工。第一不要在一开始就试图支持无限画布和无限缩放的“白板化”体验。有限画布 网格 明确的边界约束让整个交互系统简单了不止一个量级而且实际用户根本不会去用到“无限”他们更需要的是“清晰”和“稳定”。第二不要在画布渲染上过早拥抱 WebGL。Canvas 2D 在几千节点的规模下足够流畅渲染层代码也更接近业务语义后面发现问题再局部升级更划算。第三所有业务数据从第一天就要和视觉坐标分离。这个决定在项目中期开始带来巨大回报——撤销/重做、多人协作、视图切换、布局算法每一块都因为这条底线而变得干净利落。如果你也准备做自己的 diagram 系统我的建议是从一个很小的场景切入比如只做“流程图编辑”先把增删改查、拖拽连线、保存导出这条链路打通再逐步抽象出节点注册表和主题 Token。千万不要一开始就想着“我要做一个无所不能的画板”那样的项目大概率会卡在第二个月。diagram-design 到这里也只是完成了一个相对稳定的第一版支持多种图类型、流畅的交互、统一的视觉体系、可插拔的节点能力以及最基础的多人协同。下一步我会继续补更多“真实业务场景”的适配比如把代码依赖自动布局、监控状态实时刷新这些能力做成通用模块。可视化这块领域水比想象中深但把地基打牢之后每一次扩展都会比以前更从容。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑