diagram-design 这个名字其实是我当时随手起的项目代号,后来用习惯了就没再改。起因特别朴素:团队里那段时间画架构图、业务流程图、部署拓扑图的频率极高,可每次画图都像打一场小仗。产品同学习惯用在线白板,觉得功能越多越花哨;后端同事只想要命令行能跑的脚本式生成;前端组觉得代码驱动的图改起来太累。手里换过七八个工具,始终没有一个能照顾到所有人。做 diagram-design 的初衷,就是想折腾一个让工程师和产品经理都能上手的 Web 图表设计器,把"随便画画"和"画得规范"这两件事揉到一起。
这篇内容主要面向两类人:一类是想自研内部工具的前端开发者,另一类是每天和架构图、流程图打交道的产品与研发同学。我会把整个项目的定位思考、技术选型、核心实现和踩过的坑都摊开讲清楚,涉及的代码片段都是简化过的思路版本,真正动手做的时候可以直接参考。
1. 项目开始前的定位思考
1.1 市面上图表工具的三个普遍痛点
动手之前,我先把团队里所有人用过的主流工具都拉了一遍,集中吐槽的点基本可以归为三类。
第一类是"重"的工具太复杂。像传统桌面绘图软件,功能确实强大,但启动慢、需要安装,而且大量菜单和面板对只想快速画一张部署图的人来说完全多余。每次打开都有一种"为了拧个螺丝,搬出了整套工具箱"的沉重感。
第二类是"轻"的工具太随意。网页版白板类工具上手快,但画出来的东西往往不严谨。节点之间没有吸附对齐,连线没有自动路由,想导出一张可以放进技术方案文档里的图,总得手动整理半天。对于需要严肃交付的场景,这种随意感会拉低沟通效率。
第三类是"代码化"的工具对非技术用户不友好。用代码描述图确实适合版本管理,但让产品同学或者刚来的实习生用这种工具画业务流程图,学习成本立刻变成一道门槛。
这些痛点综合起来就形成了一个模糊的诉求:既能像在线白板那样随手可用,又能产出接近专业绘图软件质量的输出结果,最好还支持团队协作和模板复用。diagram-design 就是冲着这个交集去的。
1.2 项目定位:不做全能编辑器,先解决高频场景
如果一开始就想着做一个面面俱到的全能绘图编辑器,这个项目大概率会烂尾。我的做法是先圈定一小块高频场景,把这块做得足够顺手,再逐步扩展。
初期锁定的场景是技术团队最高频的三类图:架构图、流程图、时序图。架构图用来表达系统模块和依赖关系,流程图用来梳理业务流程,时序图用来描述跨系统交互。这三类图基本覆盖了日常方案设计和技术文档的大部分配图需求。
围绕这三类场景,我给 diagram-design 定了几条边界。不做非常复杂的自由绘图,不搞涂鸦和画笔类功能,不追求无限画布的多人在线协同(这个工程复杂度太高,一期先放弃)。把这些都排除掉之后,核心功能就变得很清晰:节点和连线的创建与编辑、样式系统、自动布局、导入导出、模板沉淀。
这些边界让项目在三个月内就能跑出第一个可用的内部版本,而不是陷入"功能越加越多、越来越不顺手"的无底洞。
1.3 目标用户与使用场景拆解
diagram-design 从设计之初就确定了一个交互原则:鼠标能用,键盘更好用,触摸设备基本不用。目标用户以桌面端的研发和产品同学为主。
围绕这个原则,我把用户分成了三类,每一类的核心诉求都不同。
研发同学(前端、后端、架构师)最关心的是能不能用代码批量生成图,能不能用统一的 DSL 描述系统架构,以及生成的图能否导进文档里。产品经理和运营同学更在意的是画业务流程图和交互链路图时的易用性——能不能拖拽生成节点,能不能快速连线,样式是否清爽美观。还有一类是技术文档维护者,他们关注的是模板沉淀、批量更新以及与其他文档工具的集成。
针对这三类人,操作逻辑和使用路径做了分层。鼠标拖拽是默认入口,工具栏精简到只有最常用的七八个操作;进阶用户可以用快捷键和批量编辑;基础设施层面预留了 JSON 格式的导入导出接口,方便有代码能力的用户跳过 UI 直接生成和修改图。
2. 技术选型与整体架构设计
2.1 渲染层选型:为什么选了 Canvas 而不是纯 SVG
这是 diagram-design 遇到的第一个关键取舍。渲染层在 Canvas 和 SVG 之间,技术圈已经有很多讨论,真正落到这个项目上,我的判断依据主要有三点。
SVG 的优势在于 DOM 元素级别的交互能力,每个节点天然是一个独立 DOM 节点,绑定事件非常直接,而且样式可以用 CSS 完全控制,对熟悉前端的人几乎没有学习成本。但 SVG 在画布规模变大之后性能下滑明显,几百个节点加上连线还能扛,到上千个节点时,任何一次的 transform 操作都可能触发大面积重绘,交互流畅度会肉眼可见地下降。
Canvas 的优势是它的绘制方式更接近"画板"模型,每次重绘都是整体刷新,只要控制好绘制区域和渲染逻辑,性能天花板明显比 SVG 高。它的劣势也很直观——没有天然的节点对象,所有命中检测、事件分发都得自己实现,开发成本更高。
我最终选择了 Canvas 作为主渲染层,同时在极小部分交互场景保留了一个隐藏的 SVG 层用于辅助。比如鼠标悬停时的高亮描边、选中框的绘制,这些高频但轻量的交互用 SVG 实现非常方便,不会影响整体的 Canvas 性能。这种混合方案在早期版本里帮我省了很多事。
选型的背后逻辑很简单:diagram-design 计划承载的图规模是成百上千个节点,而不是常见的几十个节点的小图,Canvas 的性能余量更适合这种规模。如果你做的是一个专注小图编辑、强依赖 DOM 操作的轻量工具,SVG 可能是更划算的选择。画布大小、节点规模、交互复杂度,这三个变量决定了选型方向,没有绝对的对错。
2.2 自研交互核心:成熟图编辑库不是银弹
项目刚开始时,我考虑过接入开源的图编辑库,毕竟现成的东西可以少走很多弯路。试了一圈之后发现一个问题:这些库的扩展点往往被它们自身的设计理念限制住了,想实现一些定制能力反而比从零开始更别扭。
比如,大部分图编辑库把"节点"和"连线"作为一等公民封装得很完整,但我想做的是让节点拥有不同类型的端口(输入、输出、双向)、让连线能自动绕过遮挡物、让布局引擎能感知节点之间的逻辑层级。这些需求在通用库里面改起来费劲,有些甚至需要修改库源码,维护成本很高。
另外一个现实因素是团队对核心代码的掌控要求。自研之后,任何交互细节的调整都只需要改自己的代码,不依赖第三方库的更新节奏。虽然初期工作量变大了,但后面迭代的速度优势很快就体现出来。
最终决定是全部自研交互层:节点命中、框选、拖拽、连线、缩放平移、撤销重做,全部自己实现。碰撞检测算法、坐标转换、矩阵变换这些底层能力直接基于原生 API 自己封装,不依赖任何重型图形库。核心代码大约一万行出头,复杂度完全可控。
2.3 数据层设计:一份模型,多处消费
图表工具的底层是数据模型,而不是渲染层的绘制代码。diagram-design 的数据模型从一开始就设计成与渲染层解耦,用一份统一的 JSON Schema 描述图的状态。
核心数据结构分为三层:画布层(画布大小、背景样式、缩放级别)、节点层(节点 ID、位置、尺寸、样式、绑定的业务数据)和连线层(连线 ID、起点终点端口、路径点、样式、标签信息)。
这个设计的直接好处是:导入导出 JSON 只需要做序列化和反序列化,不需要关心渲染层的具体实现;自动布局引擎可以直接基于数据模型做运算,输出结果再回写到模型里;如果未来想增加某种新的渲染模式(比如导出成纯代码文件),也只是从模型翻译到目标格式的问题。数据模型作为唯一事实来源,让整个系统结构变得非常清晰。
3. 核心功能拆解与实操实现
3.1 从零搭建画布与节点的第一步
画布的基础能力是平移和缩放,这是所有后续交互的地基。
实现核心是维护一个 viewport 变换对象,记录当前的缩放比例和原点偏移量。每次鼠标拖拽移动的是画布内容而非画布本身,实际修改的是 viewport 的偏移值。所有鼠标坐标都需要经过一个 screenToWorld 方法转换到世界坐标,再拿去和节点位置做计算,否则缩放之后节点会被"拽跑"。
这里有一个关键细节:Canvas 绘制的时候,先把当前状态按 viewport 参数做 translate 和 scale,然后所有节点的绘制逻辑完全基于世界坐标,不需要在节点代码里反复换算。这个模式会让代码清晰很多。
节点交互的第一步是命中检测。Canvas 没有现成的"点选某个图形"能力,我采用的做法是:每次鼠标点击时,遍历所有节点,判断点击点是否落在节点的包围盒范围内。几百个节点的遍历量完全可接受,如果节点规模上了五千,这里需要再做一层空间索引优化。
实现这套基础交互大概花了一周时间,核心文件只有三个:viewport 控制、节点数据管理、渲染循环。先把这一步跑通,后面的功能都是在这个地基上长出来的。
3.2 拖拽、缩放与连线交互的完整闭环
节点的拖拽看似简单,实际实现时有一个很容易忽略的细节:拖拽过程中,节点需要从固定状态切换到"跟随鼠标"状态,同时要保证鼠标按下位置和节点被拖动位置之间的相对偏移不变,否则会出现"节点跳到鼠标旁边"的突兀感。
我采用的做法是:在 mouse down 时记录点击点在节点上的相对坐标,然后在每次鼠标移动时用当前世界坐标减去这个相对坐标,得到节点的新位置。这样拖拽手感非常精确,不会出现节点偏移跳动。
连线功能是图表编辑器里最有交互感的环节。我实现的是"锚点到锚点"的连接方式:每个节点定义若干端口(上下左右四个方向),拖出一条临时连线时实时计算最近的可用端口并显示吸附提示。释放鼠标时,如果吸附到目标端口则创建一条连线,否则取消操作。
连线绘制采用的是贝塞尔曲线,根据起点和终点端口的方向自动生成控制点,曲线看起来会比较柔和自然。时序图场景还额外支持了折线形式,方便表达生命周期消息。
3.3 自动布局:让手残党也能画出一张整齐的图
diagram-design 有个打动人的细节:自动布局。画图这件事最苦恼的是对齐,尤其节点一多,手动拖到眼瞎也未必整齐。自动布局的价值在于牺牲一小部分控制感,换取整体视觉的规范性和可读性。
第一版先实现了两种最常用的布局算法。层级布局适用于流程图和架构图,核心思路是把节点按上下(或左右)依赖关系分层,同一层的节点纵向对齐,层间距固定,节点在同一层内均匀分布。实现采用的是经典的层次化布局思路,经过若干轮排序松弛迭代后效果已经足够好看。力导向布局适用于拓扑图和无严格层级关系的场景,通过模拟弹力和斥力迭代让节点自动分布,连线长度会比较均匀,模块聚合效果也自然。
布局完成之后还有一个关键步骤:把布局结果写回数据模型,并保持节点之间的连线路由随之更新。这一步做完之后,用户点击一次"自动布局"按钮,整张图就焕然一新。实测下来,产品同学和研发同学的使用率都很高,这个功能在内部收获了很多好评。
3.4 序列化格式与增量保存
图表编辑器的数据持久化看起来很简单,实际要考虑的细节不少。白板不能无限变大,节点位置需要归一化处理,历史记录需要支持撤销重做,多人协作时还要保证并发不冲突。
diagram-design 采用了一套版本化的 JSON Schema。所有序列化数据都带一个 formatVersion 字段,每个版本的解析器独立维护,保证旧文件在升级之后依然可以打开。节点和连线都带唯一 ID,任意一次修改都会生成一次完整快照,存储到撤销栈里。
关于增量保存,我选择了一个取巧的方案:每次操作完成后只保存修改过的节点和连线,而上层通过一个全量快照加增量操作记录的方式合并。这样即使某个操作中途失败,也能从上一个快照恢复,不会丢数据。这套方案在单机保存和 Web 端 localStorage 持久化场景下都表现稳定,实测没有出现过多大问题。
4. 踩过的坑与排查实录
4.1 连线锚点偏移与吸附灵敏度问题
刚让内部用户试用时,最集中的反馈是"连线总感觉对不齐"。排查之后发现是锚点吸附的判断逻辑太粗糙。当时只判断了鼠标位置距离端口中心点是否在 10 像素以内,一旦画布缩放比例改变,这个 10 像素的绝对距离在不同缩放级别下表现不一致。
修正的做法是:吸附判断改为以世界坐标为标准,并让吸附范围随缩放比例动态调整——缩小画布时范围增大,放大画布时范围减小。同时改进了端口可见性逻辑:只有当鼠标靠近时才显示端口点,避免端口点干扰视觉。
实测下来,连线操作准确率提升非常明显。类似的"绝对距离 vs 世界坐标"的问题,在图表编辑器里会反复出现,越早统一换算逻辑,后面踩的坑越少。
4.2 大画布拖拽卡顿与性能瓶颈
单个 Canvas 在节点数量少于两百时能保持 60fps,但超过三百之后,拖拽画布会明显掉帧。性能分析发现主要瓶颈不在绘制本身,而在于每次重绘时对所有节点做完整遍历和样式解析。
优化思路分了三个层面。第一层是跳过不可见节点:计算当前视口在世界坐标中的范围,只绘制与视口相交的节点。这个优化在画布很大、节点分散时效果特别棒。第二层是样式缓存:所有节点和连线的样式对象预计算一次并缓存,绘制时直接读取,不再重复解析。第三层是绘图批处理:同一类节点合并绘制路径,减少 canvas 状态切换次数。
这三层优化合在一起,把五百节点的拖拽体验从明显卡顿优化到相对流畅。2000 节点级别的极端场景仍然会有些吃力,但保障 500 节点以内的流畅已经能覆盖大多数用户的真实需求。
4.3 撤销重做栈的设计失误
第一版撤销重做实现得很简单:每次操作前把全部节点和连线序列化成一个快照,push 到撤销栈里。这个方案在小图场景下没问题,但一旦图变大,快照体积会急剧膨胀,内存消耗也跟着失控。
改版时我把"全量快照"换成了"操作命令"模式。每个操作(增删节点、移动节点、改变样式、修改连线)都抽象成一个命令对象,执行命令时同时记录逆操作。撤销时执行逆操作,重做时再执行原操作。内存消耗大幅下降,撤销精度也从"恢复到某一步"提升到了"精确撤销某一步"。
这里有一个心得:图表工具的撤销重做一定要从第一天就按命令模式设计,等快照模式写成形了再回头改,会非常痛苦。
4.4 浏览器兼容性:不只是 Canvas 的问题
Canvas 本身在现代浏览器里兼容性不是问题,实际遇到的坑都在周边能力上。比如不同浏览器对 wheel 事件的 deltaY 单位处理不同,有些浏览器还会把触摸板的横向滚动误判为缩放操作。处理方式是统一对事件参数做归一化,并对缩放加的节流控制做防御判断。
另一个坑是拖拽过程中触发了浏览器原生的文本选择或者图片拖拽默认行为,导致鼠标事件被吃掉。解决方式是在画布容器上统一管理 draggable=false 与 user-select 样式,并对事件循环做 preventDefault 处理。
这些兼容性问题不具备很强的技术含量,但每一条都会消耗不少调试时间。整理成一个已知问题清单,后续维护效率会高很多。
5. 实测体验与后续规划
5.1 内测阶段的使用反馈分析与调优
内部试用两个月之后,我从后台日志和使用反馈里总结了几个有意思的现象。第一个现象是自动布局的使用频次远超预期,很多用户手动拖了一两分钟之后就直接点自动布局,宁愿牺牲一定的个人调整空间也要换取视觉效果的整体统一。这说明在新手期,"整齐"比"自由"更能带来满足感。
第二个现象是模板复用率很高,尤其是部署图和架构图模板,基本每个团队都会在它基础上做二次修改。受此启发,新版本加强了模板系统的建设——每一个项目都可以把当前画布存成个人模板,也可以把任意一段内容作为组件封装,后续在画布里直接拖出使用。
第三个现象是 JSON 导入导出的使用量比预期大。有不少前端工程师直接把 JSON 接入到自己的自动化流程里,用脚本生成图表数据再灌入 diagram-design 渲染。这让我意识到,这类工具的长期价值很大一部分在于它能否嵌入到用户的工程链路中,而不仅仅是一个孤立的绘图工具。
5.2 最值回票价的三个小设计
回顾这个项目全程,有三个设计投入产出比特别高,值得单独拎出来说一说。
第一个是把端口设计成可配置的样式而非固定样式。默认提供圆形、方形、菱形三种端口形态,每种端口都可以单独绑定输入输出等语义。这个能力让 diagram-design 在不同业务场景之间有了很强的适配性,架构图和时序图在表达层用了同一套基础能力。
第二个是连线的标签系统。连线上可以添加文本标签,并且标签可以设置跟随连线方向自动旋转,也可以强制保持水平。技术方案里的时序图和架构图经常需要在连线上标注接口名、协议类型、依赖说明,这个能力直接命中了高频刚需。
第三个是导出能力,内置了 PNG、SVG、JSON 三种导出格式,并且支持去除背景色透明导出。透明背景导出的 SVG 可以直接放进技术文档和设计稿中使用,避免了"白底卡片截图"带来的违和感。这三种格式覆盖了预览、矢量、数据三个层面的需求,让一张图可以无缝衔接到不同的交付渠道。
5.3 后续可以扩展的方向
diagram-design 目前的版本还有几个明确的后继方向。第一个是把数据模型和组件库进一步开放出来,让外部使用者可以用自己的前端组件替换默认节点渲染。第二个是补上多人实时协同编辑,基于现有的命令模式做数据同步,比从快照体系迁移要自然得多。第三个是加强智能分析能力,比如根据节点之间的依赖关系自动检测循环引用、找出孤立节点、建议连线方向等,这些在架构治理场景里会有实际价值。
如果你也准备做类似的内部工具,我的建议是:先忍住"什么都要做"的冲动,抓住最高频的十个场景做到让人顺手,剩下的交给使用反馈去驱动。工具的价值不在于功能多,而在于能在真实的工作流里被高频使用,被抱住不放。diagram-design 还有很长的路要走,但它已经证明了一件事——一个聚焦核心场景、交互顺畅的图表设计器,是真的能解决团队协作中的很多实际问题。