
diagram-design这个名字乍看平平无奇但如果你每天要画架构图、流程图、数据流图就会明白一个能用几行文本生成图表的项目到底能省下多少时间。这不是又一个拖拽式画板而是一个把图表描述和渲染彻底解耦的小型系统——你用结构化的文本定义节点、连线、分组和样式工具负责自动排版、渲染和导出。它可以做的事很多写技术方案时顺手生成一张当前系统的服务依赖图评审时直接对图提 comment而不是截图贴到群里扯皮甚至把它接进 CI让每次构建都自动产出一份最新的拓扑图。适合谁来参考给经常画图的人省时间给想把图表沉淀成代码资产的人一个新思路也适合正在做工具类项目的人看看一个轻量设计是怎么落地的。1. 项目到底想解决什么问题1.1 传统画图工具的痛点你肯定也遇到过先说一个很常见的场景某次架构评审前夜你把架构图画好了躺床上突然想起某个服务其实已经拆成了两个于是第二天早上你得打开画板手动拖动框线、重新连线、把文字一个个拽回到框里可能还要因为箭头交叉而绕一大圈折线。这种图比代码还难改的体验是很多人提到画图就头疼的根源。传统画图工具做的是所见即所得好处是上手快坏处是它把图当成了一张一次性画布。图里每一个元素的位置、颜色、字体、连线的拐点全都变成不可复制的手工作坊资产。一旦内容变化你要么重画要么花大量时间微调布局。更麻烦的是协作——你发给同事一张图同事想修改只能基于图片里的信息重新画一份版本一多根本分不清哪张是最新的。我在这个项目中体会最深的一点是图的本质不是画出来的样子而是元素之间的关系。只要把关系描述清楚长什么样、摆在哪、怎么配色都是可以自动推导的。diagram-design 的出发点就是把重心从画拉回描述。1.2 diagram-design 的核心设计取舍当时搭这个项目时我最先定的原则是绝对不做拖拽编辑器。不是拖拽不好而是拖拽编辑器解决的是临时画图的问题解决不了图表要跟随文档长期演进的问题。我的目标非常明确让图表像代码一样存放在版本仓库里可以被 diff可以被 review可以让任何人在几分钟内通过文本理解图的结构。所以整个设计建立在三个核心取舍上第一声明式描述优先。你写的不是画布上每一个像素的位置而是系统 A 依赖系统 B它们之间存在同步调用关系排版的事情交给布局算法。这个思路和写 CSS 类似——你描述样式浏览器负责渲染不自己去算每个元素在哪个坐标。第二自动布局为主手动微调为辅。自动布局必然不是完美的任何布局算法面对复杂图都会出现交叉和拥挤所以系统里保留了手动锚点、固定层级、节点分组这样的提示机制让用户可以通过配置干预布局而不是彻底放任算法。第三以文本为单一事实来源。图表渲染的结果只是产物源文件永远是那份文本。这意味着你可以把一个图嵌进 Markdown 文档可以放在仓库里做版本对比可以写个小脚本批量生成几十张图——这是位图或者画板工程文件都做不到的。2. 核心功能拆解与选型思路2.1 灰度不同的图表类型与语法设计做一个图表系统第一步是确定支持哪些图表类型。这个项目最初只支持两种流程型和拓扑型。流程型适合描述步骤、状态流转、审批链路拓扑型适合描述服务依赖、网络结构、模块关系。后来逐渐加了分组容器图把一组节点圈在一个大框里表示子系统或泳道、ER 图实体和实体间的关系基本上覆盖了日常技术写作 90% 的需求。语法设计是这里的灵魂。我参考了几个图表即代码的既有方案发现它们的表达方式各不相同但共同点是都有定义节点和定义连接两步。我的语法设计尽量往直觉靠拢。节点可以用一个简单的标识符加上可选显示文本连接用箭头符号表达方向// 定义三个服务节点 用户端; 网关; 订单服务; // 定义依赖关系 用户端 - 网关; 网关 - 订单服务;如果要加备注、加样式可以用大括号扩展属性用户端 { 样式: 边框虚线; 说明: 外部访问入口; } 订单服务 { 颜色: #2d8cf0; }为什么这么设计因为我希望语法具备只看一遍就能背下来的能力。图表文本如果写得太复杂让人感觉像在写 XML那用户宁可回去手动画图。简洁本身就是这类工具最重要的可用性指标。2.2 渲染引擎与导出链路图表定义好了之后接下来的问题是怎么变成一张漂亮的图这一步我把它拆成两个子问题布局计算和图形渲染。布局计算解决的是每个节点画在什么位置。流程型图表用分层布局先按节点之间的依赖关系划分层级保证箭头尽量朝同一方向再在每一层内优化排列顺序减少交叉。拓扑型图表则用力导向布局给每个节点一个初始位置然后模拟相连的节点互相吸引、不相连的节点互相排斥的物理过程迭代若干轮后达到一个相对稳定的平衡状态。这两种布局算法是常见开源方案的标配自己实现的核心部分是对它们的参数做了可调暴露比如迭代次数、节点间距、斥力系数让用户在遇到复杂图时能主动干预。图形渲染解决的是怎么把位置画成像素或矢量。我的实现里走的是 SVG 渲染。为什么不选 Canvas核心原因在于 SVG 是矢量格式缩放不糊可以被搜索引擎索引、被屏幕阅读器读取而且可以直接嵌入网页和 Markdown 文档。另一个原因是 SVG 的 DOM 结构天然适合做交互如果以后要支持点击节点跳转、悬浮展示详情成本比 Canvas 低很多。导出链路支持三种格式原始 SVG、通过后端转换出的 PNG、以及一份纯文本的节点/边列表用于机器读取和统计分析。PNG 的生成我直接用了一个独立的图像转换工具前端的系统只负责产出干净的 SVG不掺合位图打包的逻辑。这样职责单一SVG 本身也更容易做单元测试。2.3 主题与样式系统的思考样式系统是我当初差点略过、后来发现极其重要的模块。图表工具如果只有默认的黑白样式用户生成的图会千篇一律但如果像画板一样开放所有属性又会丧失图源于代码的规范化优势。最终的方案是引入了主题这个概念。一套主题包含背景色、节点填充色、边框色、线条颜色、字体族、字号、间距系数。官方内置了三套主题默认浅色、深色适合夜间分享和投屏、极简黑白适合打印。用户也可以自定义主题文件通过 JSON 覆盖任意变量。这个设计背后的考虑是让团队可以维护统一的视觉规范比如公司主题色、标准字体然后所有图自动遵守——这比每个人手工调出五种蓝色要省心得多。主题适用场景特点默认浅色文档插图、评审材料对比度均衡适用性广深色主题投屏展示、晚间工作深色背景重点高亮突出极简黑白正式文档、打印去掉一切装饰只留结构自定义主题团队规范、项目定制变量驱动一处修改全局生效3. 从零跑通一个最小实现3.1 项目结构与模块划分这个项目我按照解析—构建—布局—渲染—导出五个阶段来组织代码每一阶段对应一个独立模块模块之间通过数据结构衔接不共享可变状态。目录结构大致是这样的diagram-design/ ├── src/ │ ├── parser/ // 文本语法解析产出元素树 │ ├── model/ // 元素树的数据结构与校验逻辑 │ ├── layout/ // 布局算法层内排序、力导向模拟 │ ├── render/ // 从布局结果生成 SVG 字符串 │ ├── theme/ // 主题定义与变量合并 │ └── exporter/ // 导出 SVG/PNG/列表文件的入口 ├── tests/ // 解析、布局、渲染的单元测试 └── examples/ // 自带示例方便快速上手为什么要按阶段拆因为这样做每个环节都能独立测试。解析阶段出问题时不需要去查渲染代码布局效果不好时可以把同一棵树喂给不同布局算法对比结果。这种流水线式的架构对工具类项目特别友好也方便后来者往里面加新的图表类型——你只需要保证输出符合 model 层的数据结构就行。3.2 核心流程从文本到元素树再到布局整个流程可以用一条主线串起来。第一步是解析文本这一步的本质是把有结构的字符串变成有结构的内存对象。我的解析器没有用正规文法或者复杂的词法分析工具而是自己写了一个简单的逐行扫描器识别节点定义行、连接行、分组块和属性块。因为语法本身就刻意设计得很简单手写解析器反而更好维护出错时的报错信息也更容易写得贴心。解析得到元素树之后要做一次数据校验。比如检查是否有节点被定义了两次、连接指向的节点是否都不存在、分组是否有循环嵌套。这一步很有必要因为后续布局算法对数据的合法性很敏感一个悬空的引用就能让布局过程直接崩溃提前校验能把问题暴露在更友好的阶段。布局阶段是最有意思的。以层级布局为例第一步是拓扑排序确定每个节点所处的层级序号。接着遍历所有跨层连接给它们做虚拟节点处理——跨层连线被拆成多段短线段这样后续的排序过程可以把连线交叉最小化当做一个排序问题来解决。最后根据层高和节点大小计算每个节点的具体坐标。这些算法本身是经典图论算法但在工程落地时大量的时间花在调参上层间距设多大、节点之间的水平间隙是多少、如何处理孤立节点不参与分层。3.3 渲染成 SVG 的几个关键细节当每个节点都有了坐标之后渲染阶段就相对直接了。我会遍历元素树将每一个节点渲染成一个 SVG 图形矩形、圆角矩形或圆形取决于类型设置将连接渲染成带箭头的路径。这里有一个非常值得注意的细节箭头。很多人画图时觉得箭头差不多对着就行但用程序画箭头时如果箭头的尖端直接对准目标节点的中心点图形中心箭头会被节点方块盖住看起来就像陷进了矩形里。解决方案是先计算出连线与目标节点边框的交点再让箭头终止于这个交点。这一步需要根据节点的形状类型矩形、圆形、圆角矩形分别做几何计算。听起来简单实际坑很多因为你画的不光是直线还有曲线路径曲线的终点方向要靠当前切线方向来确定。另外一个细节是 Clip 和遮挡关系。当节点尺寸比较小、文字比较长时文字会溢出边界。我的处理是给节点内的文本加上最大宽度限制超出部分截断用省略号表示鼠标悬浮时可以查看完整内容在支持交互的环境下。这个策略虽然牺牲了一点展示完整性但保证了大图中不至于出现大量文字重叠遮挡的烂图。渲染完成后把 SVG 元素拼接成字符串写入文件即可。SVG 的 header 部分要注意加上xmlns命名空间声明否则某些工具打开会直接报错或不显示。4. 实操中躲不开的坑4.1 自动布局那点薛定谔的乱自动布局算法在简单图上是惊艳的在复杂图上就是薛定谔的乱——你永远不知道它这次会不会给你一个满意的结果。我自己被这个问题困扰了很久。后来总结出的经验是不要和布局算法硬刚要给它引导。布局算法最早只接受节点和边后来我给 model 层加了几个非强制属性比如节点建议方向left/right、层级优先级分组锚定。用户可以在文本里写出网关 { 首选层级: 1; } 订单服务 { 首选层级: 2; }布局算法在计算时会把首选层级作为强约束加入拓扑排序这样用户就能通过微调几个关键节点把整张图的结构拉整齐。这个提示机制比给每个节点设置绝对坐标要优雅得多因为它的意图明确——用户干预的是逻辑结构而不是像素位置。提示遇到布局乱时先别改算法参数。先试着把关键的骨架节点加上层级或方向提示大多数情况下比调参更有效。4.2 中文、特殊字符与字体的隐藏问题如果你画的是中文图表字体这块大概率会踩坑。最早做出来的 SVG 在电脑上打开很正常但某次在同事的电脑浏览器里打开所有中文都变成了方块排查了很久才发现是字体回退的问题。SVG 里如果没有显式指定字体渲染引擎会依赖系统默认字体但不同系统默认字体差异很大Windows、macOS、Linux 各不一样。最终方案是在主题配置里增加几组字体栈设置{ fontFamily: PingFang SC, Microsoft YaHei, Noto Sans CJK SC, sans-serif }这样在不同系统上都能优先匹配到系统中文字体实现相对一致的显示效果。另外如果图表文本里包含类似、这样的字符在生成 SVG 时如果不做转义会让整个 XML 解析出错。所有用户输入的内容我都强制经过一道 HTML 实体转义这既是安全要求也是 SVG 合法性的基本要求。4.3 大图场景下的性能与内存问题画一张二十个节点的图毫无压力但当你试图用程序批量生成几百个节点的依赖全景图时性能问题就冒出来了。力导向布局算法的时间复杂度大约为平方量级每次迭代都要计算所有节点对之间的斥力几百个节点时画画还算流畅上千个节点时迭代一轮就可能是好几秒。我的处理办法是分三层优化。第一层是减少迭代次数——不再等到完全收敛而是设定一个最大迭代轮次并且在每轮结束时计算节点移动的平均距离如果低于阈值就提前终止。第二层是空间分区——把画布切成网格只计算相邻网格内的节点斥力大幅减少无效计算。第三层是降低 SVG 复杂度——对于密集区域用一条路径合并多条连线减少 DOM 节点数量。这里有个很关键的取舍大图的渲染质量首先保证的是能看懂结构而不是每一个节点都精致。几十个节点的小图宽松间距和饱满样式是优点几百个节点的大图上同样的样式会让整张图变成一坨色块。我后来引入了密度感知的样式策略当节点数量超过阈值时自动缩小节点面积、压缩间距、去掉圆角和阴影只用最小视觉元素表达结构。4.4 常见问题速查表现象可能原因解决办法中文渲染成方块缺少中文字体配置在主题中设置字体回退栈覆盖常见系统中文字体导出的 SVG 在某些工具里打不开缺少 XML 命名空间或实体未转义检查svg头部的xmlns属性并转义特殊字符节点顺序错乱、箭头乱指没有进行拓扑排序就进入布局先做拓扑排序再处理孤立节点连线盖住节点文字箭头端点计算未处理节点边界计算连线与节点形状的边界交点以此为终止点大型图渲染卡顿布局计算量过大、SVG 节点过多开启空间分区、减少迭代、使用密度感知样式手动调整的部分总被布局覆盖布局算法优先权高于手动提示检查是否有强制布局参数保留手动锚点的最高优先级5. 从项目到日常工作的延伸5.1 让图表变成可协作、可评审的沉淀物做这个项目之后我最大的感触是图表一旦变成代码资产它的使用方式就完全变了。以前大家在群里发架构图图片遇到改动只能发一个新图还要嘴说以最新这张为准。现在某项目把系统架构图写成了 diagram-design 的声明文件放在仓库的 docs 目录里。有人改了服务依赖关系提交的 diff 里能直接看到哪一行从服务A - 服务B变成了服务A - 服务C。评审的时候对着文件改直观且不容易漏。这种模式下图表的生命周期和代码完全同步了。自动构建时顺手渲染一张最新的架构图上传到内网知识库任何时刻看到的那张图都对应着仓库的某一 commit。我后来在一个团队内部推广这个流程时重点强调的其实是这一句话别再把图当图把它当成可视化视图源文件图的维护成本就能压缩到几乎为零。5.2 后续还可以扩展的方向如果说这个项目现在还有哪些明显值得做的改进我列了三个方向。第一个是交互式浏览模式。现在的渲染结果是一张静态 SVG但 SVG 本身是支持事件绑定的。可以做一个简单的预览页面支持点击节点高亮相关链路、拖动画布、放大缩小、查看节点关联的属性详情。这些功能不会影响核心架构只需要在渲染层输出带数据属性的 SVG 节点即可。第二个是模板化生成。很多图的拓扑结构其实是有规律的比如前端项目里的依赖图、微服务调用链。可以抽几个常用模板用户只需要填关键内容就能生成一整套图。比如微服务架构模板网关层、业务层、数据层各放几个节点再填依赖关系。第三个是集成到文档工作流的插件。如果文档系统支持嵌入自定义节点就能直接把 diagram-design 的源文件嵌进页面实现文档里看到的图就是本地源文件渲染出来的。这样图就和文档彻底绑定不再有截图同步问题。最后再分享一个我在做这个项目时意外学到的小技巧别急着从零实现布局算法。很多图表系统的核心价值不在算法本身而在于语法设计、样式系统和工程化集成。把布局算法换成现成的开源库完全不影响项目的独特价值反而能帮你把精力聚焦在真正有差异的地方。做工具类项目尤其容易陷入什么都要自己写的冲动但克制住这个冲动产品反而走得更远。