图即代码:diagram-design 从零构建可版本控制的架构图 1. 从“diagram-design”这个标题说起它到底在解决什么问题第一次看到“diagram-design”这个标题我脑子里蹦出来的不是某个具体工具而是一类非常典型的工程需求把脑子里那张模糊的架构图、流程图、时序图快速变成一张能放进文档、能给别人讲清楚、还能持续维护的图。这件事听起来简单但真正做过项目的人都知道画图这件事的痛点从来不在“画”而在“改”和“对齐”。我见过太多团队在项目初期用鼠标拖拽出一张漂亮的架构图结果两周后系统加了一个缓存层那张图就彻底废了没人愿意再去拖一遍。也见过有人用纯文本描述流程写了三千字读者看完还是不知道数据从哪来到哪去。diagram-design 这个方向要解决的就是这两者之间的鸿沟用代码或结构化描述来定义图让图具备可版本控制、可复用、可自动化生成的能力。它适合谁我认为有三类人最该关注。第一类是后端或架构方向的开发者日常要输出大量系统设计文档第二类是技术写作者和文档维护者需要保证图文一致第三类是对“文档即代码”理念感兴趣的工程师想把图也纳入 CI 流程。不管你是哪一类核心诉求都一样图的源头应该是逻辑而不是像素。这篇文章我会从设计思路、核心细节、实操过程、问题排查四个层面把 diagram-design 这类项目从零到一讲透。里面涉及的工具选型、参数配置、踩坑经验都是我在实际项目里反复验证过的你可以直接抄作业也可以按自己的场景做裁剪。2. 整体设计与思路拆解为什么“图即代码”是更优解2.1 传统画图方式的三条死路在讲 diagram-design 的设计思路之前先说说为什么传统方式走不通。我把常见的画图方式归为三类每一类都有硬伤。第一类是纯手工拖拽工具。这类工具上手快视觉效果可控但问题在于图的“真相”存在于二进制文件里。你没法在代码评审里 diff 一张图没法在合并请求里看到“这次改动把数据库从单点改成了主从”。一旦画图的人离职那张图就成了黑盒谁也不敢动。第二类是在文档里嵌静态图片。这比第一类更糟因为图和文字是分离的。文字改了图没改读者被误导。而且静态图片没法搜索没法复制里面的文字信息密度极低。第三类是用通用绘图库手写代码。比如用某些编程语言的绘图 API 一行行画线、画框。这种方式确实可版本控制但开发效率极低改一个节点位置要调半天坐标完全不适合快速迭代。diagram-design 这类项目的设计思路本质上是在“描述性”和“可控性”之间找平衡。它不要求你精确到像素而是让你用接近自然语言的结构化语法描述“谁连谁、谁包含谁、数据往哪流”然后由渲染引擎自动布局。这样你改的是逻辑布局交给工具。2.2 核心设计原则声明式、可版本化、可组合我总结下来一个合格的 diagram-design 方案必须满足三个原则缺一不可。声明式意味着你描述的是“图应该是什么样”而不是“怎么画出这张图”。比如你写“服务A调用服务B”而不是“在坐标(100,200)画一个矩形再画一条箭头到(300,400)”。声明式的好处是布局引擎可以根据节点数量自动调整你加十个节点图不会乱成一团。可版本化意味着图的源文件是纯文本。纯文本才能进 Git才能 diff才能做代码评审。这一点看似简单但它是整个方案能否落地的基石。我见过团队用二进制格式存图结果每次改图都要靠截图对比效率低到令人发指。可组合意味着你可以把一张大图拆成多个小模块分别定义最后拼在一起。比如把“用户服务”“订单服务”“支付服务”分别写成独立片段主图只负责引用和连线。这样不同团队可以并行维护各自的片段最后自动合成全局图。提示如果你现在的项目里图还是二进制格式建议先做一次“图源文本化”的迁移哪怕只是把关键架构图转成文本描述收益也会在三个月内显现出来。2.3 方案选型为什么我最终倾向文本驱动渲染市面上做“图即代码”的方案不少我大致分过几类一类是专用 DSL 工具用自定义语法描述图一类是通用绘图库用编程语言直接画还有一类是 Markdown 扩展在文档里直接写图。我最终倾向的是专用 DSL 加渲染引擎的组合。原因有三点。第一DSL 的语法是为图设计的写起来比通用编程语言短得多学习成本低。第二渲染引擎通常内置了多种布局算法比如层次布局、力导向布局、序列布局你只需要选一种不用自己算坐标。第三DSL 文件天然适合做模板可以把常用结构抽成片段复用。通用绘图库我不是没用过但每次改图都像在做几何题时间全花在调坐标上而不是思考架构本身。Markdown 扩展方案适合轻量场景但一旦图变复杂表达能力就不够了。所以我的建议是轻量图用 Markdown 扩展复杂架构图用专用 DSL两者可以共存。3. 核心细节解析与实操要点从语法到渲染的完整链路3.1 图的基本元素与语法结构任何 diagram-design 方案核心元素都逃不出这几样节点、连线、分组、标签。节点是图里的实体比如一个服务、一个数据库、一个模块。连线表示关系比如调用、依赖、数据流。分组用来做边界比如把同一层的服务框在一起。标签是附加说明比如协议名、端口号、备注。以我常用的一套 DSL 为例定义一个节点大概是这样service UserService { label: 用户服务 tech: Go }定义一个连线UserService - OrderService : HTTP/JSON分组group Backend { UserService OrderService PaymentService }这套语法看起来简单但有几个细节必须注意。第一节点 ID 和显示标签要分开。ID 用英文、无空格、稳定不变标签用中文、可随时改。这样你改显示文字不会影响连线引用。第二连线方向要明确。是 A 调用 B还是 B 依赖 A方向反了整张图的语义就错了。第三分组不要嵌套太深。我建议最多两层再深就该拆图了。注意节点 ID 一旦确定就不要轻易改因为所有引用它的连线都会断。如果非要改用全局替换别手动一个个找。3.2 布局引擎的选择与参数调优布局是 diagram-design 里最容易被忽视、但影响最大的环节。同一份源文件用不同布局算法渲染出来可读性天差地别。常见的布局算法有四种。层次布局适合有明确上下级关系的图比如调用链、组织架构。力导向布局适合关系复杂的网络图节点会自动散开。序列布局适合时序图按时间顺序排列。正交布局适合电路或网络拓扑连线都是直角。我实际用下来架构图首选层次布局因为它能清晰表达“上层依赖下层”的语义。但层次布局有个坑节点太多时会横向铺得太宽。这时候需要调两个参数一个是“层间距”一个是“同层节点间距”。层间距太小连线会挤在一起太大图会拉得很长。我的经验值是层间距设为节点高度的 1.5 倍同层间距设为节点宽度的 0.8 倍具体还要看渲染引擎的默认值。还有一个参数叫“边路由”决定连线怎么绕开节点。默认的直线路由在节点密集时会穿过节点非常难看。建议开启“正交路由”或“曲线路由”虽然渲染慢一点但可读性提升明显。3.3 样式与主题让图能直接进文档图渲染出来是要给人看的样式不能太随意。我一般会定义一套主题统一颜色、字体、圆角、阴影。颜色方面我遵循一个原则按语义上色不按喜好上色。比如所有数据库用同一种颜色所有外部服务用另一种颜色所有消息队列用第三种。这样读者一眼就能看出哪些是同类组件。字体方面中文图一定要确认渲染引擎支持中文字体否则会显示成方块。我踩过这个坑排查了半天才发现是字体缺失。圆角和阴影属于锦上添花但有个细节要注意阴影不要用纯黑用深灰加透明度否则图会显得很脏。边框线宽建议 1 到 1.5 像素太细看不清太粗显得笨重。提示如果你的图要同时用于浅色和深色文档建议准备两套主题或者用中性色避免在深色背景上出现看不清的深色文字。3.4 版本控制与协作规范图源文件进了 Git 之后协作规范就很重要了。我定过几条规矩效果不错。第一每次改图必须写提交信息说明改了什么、为什么改。比如“增加缓存层因为 QPS 超过阈值”。这样以后回溯时有据可查。第二图源文件和代码放在同一个仓库最好放在 docs 目录下和代码一起评审。第三渲染产物不进仓库只进源文件渲染在 CI 里做。这样仓库干净也不会出现源文件和图片不一致的情况。还有一点大图要拆文件。我见过一张图源文件两千行改一个节点要滚半天。后来拆成主图和若干子图主图只负责引用子图各自维护效率高了很多。4. 实操过程与核心环节实现从零渲染一张架构图4.1 环境准备与工具安装假设你现在要从零开始我按我的习惯走一遍流程。首先确认本地有渲染引擎我用的是命令行版本安装方式因平台而异核心是确保diagram命令可用。diagram --version如果输出版本号说明安装成功。接下来建一个工作目录结构大概是这样diagram-project/ src/ main.diagram modules/ user.diagram order.diagram themes/ light.theme output/src放源文件modules放子图themes放主题output放渲染结果。这个结构的好处是源文件和产物分离主题可复用。4.2 编写第一张图从三个节点开始我不建议一上来就画大图先画三个节点练手。打开main.diagram写入service Client { label: 客户端 } service API { label: API 网关 } service DB { label: 数据库 } Client - API : HTTPS API - DB : SQL保存后渲染diagram render src/main.diagram -o output/main.svg打开main.svg你应该能看到三个节点和两条连线。如果中文显示正常说明字体没问题。如果显示方块去主题文件里指定中文字体。这一步看似简单但它是整个流程的缩影。后面所有的复杂图都是在这个基础上加节点、加分组、加样式。4.3 引入分组与子图让结构清晰三个节点太简单现在加分组。假设我们要区分“前端”和“后端”group Frontend { Client } group Backend { API DB } Client - API : HTTPS API - DB : SQL渲染后你会看到两个框分别包住客户端和后端组件。这时候布局引擎会自动调整位置把同组的节点放近一些。如果后端组件很多可以拆成子图。新建modules/backend.diagramservice API { label: API 网关 } service DB { label: 数据库 } service Cache { label: 缓存 } API - DB : SQL API - Cache : Redis然后在主图里引用include modules/backend.diagram group Frontend { Client } group Backend { API DB Cache }这样后端团队可以独立维护backend.diagram主图只负责组装。我实际项目里就是这么干的五个团队各维护一个子图主图每周自动合成一次。4.4 样式定制让图符合团队规范样式定制我一般分两步。第一步定义主题文件themes/light.themetheme Light { background: #FFFFFF node { fill: #F5F7FA stroke: #D0D7DE stroke-width: 1.5 font-family: Noto Sans CJK SC font-size: 14 corner-radius: 6 } edge { stroke: #57606A stroke-width: 1.2 arrow-size: 8 } group { fill: #FFFFFF stroke: #AFB8C1 stroke-dasharray: 4 2 } }第二步在渲染时指定主题diagram render src/main.diagram --theme themes/light.theme -o output/main.svg这套配色我用了很久在投影仪和打印稿上都清晰。关键是字体指定了中文字体避免方块问题。圆角 6 像素是视觉上比较舒服的值太小显得尖锐太大显得圆滑。4.5 自动化渲染接入 CI 流程图源文件进了仓库下一步就是自动化。我在 CI 里加了一个步骤每次合并到主分支就重新渲染所有图并推送到文档站点。核心命令是遍历src下所有.diagram文件逐个渲染find src -name *.diagram | while read f; do name$(basename $f .diagram) diagram render $f --theme themes/light.theme -o output/$name.svg done这一步跑完output目录里就是最新的图。如果渲染失败CI 会报错阻止合并。这样能保证仓库里的图源文件和文档里的图永远一致。注意CI 环境里要确保中文字体已安装否则渲染出来的图中文会变方块。我一般会在 CI 脚本里加一行字体安装命令。5. 常见问题与排查技巧实录踩过的坑都在这5.1 渲染报错与语法问题速查图源文件写错语法是家常便饭我把常见错误整理成表方便对照排查。现象可能原因解决方法渲染报“未定义节点”连线引用了不存在的节点 ID检查 ID 拼写确认节点已定义中文显示为方块渲染环境缺少中文字体安装中文字体并在主题中指定连线穿过节点未开启边路由或路由算法不合适开启正交路由或曲线路由图横向过宽层次布局同层节点过多拆分子图或调整同层间距分组框重叠分组嵌套过深或节点归属冲突减少嵌套层数检查节点是否重复分组渲染速度极慢节点数量过大或布局算法复杂度过高拆图或换用更轻量的布局算法这张表我贴在团队 wiki 上新人遇到问题先查表解决不了再问我。大部分问题都是拼写和字体真正复杂的布局问题反而少。5.2 布局不理想的三种调整思路布局不理想是最让人头疼的因为不是报错图能出来就是不好看。我总结三种调整思路。第一种是调整节点顺序。很多布局引擎会按节点定义顺序排列你把关键节点写在前面它就会放在更显眼的位置。第二种是手动指定层级。有些 DSL 支持给节点打层级标签比如layer: 1布局引擎会按层级排列。第三种是拆图。如果一张图超过三十个节点怎么调都不会太好看不如拆成两张一张总览一张细节。我个人的经验是超过二十个节点就该考虑拆图。总览图只放核心组件细节图放具体实现。读者先看总览建立全局观再看细节深入。5.3 团队协作中的版本冲突处理多人同时改图源文件冲突是难免的。我的处理原则是小冲突手动合大冲突拆文件。小冲突比如两个人改了同一个节点的标签手动合并即可。大冲突比如两个人同时往同一个分组里加节点这时候就该考虑把这个分组拆成独立子图各自维护。拆完之后主图只负责引用冲突概率大幅下降。还有一个技巧是约定命名规范。节点 ID 用“模块名_功能名”的格式比如user_login、order_create。这样即使两个人同时加节点ID 也不容易撞车。提示如果团队用 Git建议给图源文件单独设一个 CODEOWNERS指定专人负责合并避免图越改越乱。5.4 性能优化大图渲染慢怎么办图大了之后渲染慢是必然的。我试过几种优化手段效果比较明显的有三个。第一是关闭不必要的视觉效果。阴影、渐变、圆角这些在渲染时都耗性能如果图只是内部评审用可以关掉。第二是换用更快的布局算法。层次布局比力导向快很多如果图的结构允许优先用层次布局。第三是增量渲染。有些工具支持只渲染改动的部分虽然配置麻烦但大图场景下收益很大。我实际项目里最大的一张图有八十多个节点全量渲染要十几秒。后来拆成四张子图每张二十个节点渲染时间降到两秒以内维护也方便多了。6. 从单图到图体系diagram-design 的进阶玩法6.1 建立图库与模板复用当项目里图多了之后你会发现很多结构是重复的。比如每个微服务都有“服务本体、数据库、缓存、消息队列”这四件套。这时候就该建图库了。我的做法是抽一个templates/service.diagramtemplate ServiceTemplate { param name param tech service ${name} { label: ${name} tech: ${tech} } service ${name}_DB { label: ${name} 数据库 } service ${name}_Cache { label: ${name} 缓存 } ${name} - ${name}_DB : SQL ${name} - ${name}_Cache : Redis }用的时候实例化use ServiceTemplate(name: 用户服务, tech: Go) use ServiceTemplate(name: 订单服务, tech: Java)这样加一个新服务只需要一行而且结构统一不会出现这个服务有缓存那个服务没缓存的情况。模板复用是图体系化的关键一步能大幅降低维护成本。6.2 图与文档的联动图源文件可以和文档联动这是“文档即代码”的延伸。我的做法是在 Markdown 文档里直接引用渲染产物CI 负责保证产物最新。比如文档里写![系统架构](./output/architecture.svg)CI 在构建文档前先渲染所有图这样文档里的图永远是最新的。更进一步可以在图源文件里加注释渲染时把注释作为 tooltip鼠标悬停显示详细信息。这样图既是概览又是入口。6.3 图的自动化检查图也能做自动化检查这一点很多人没想到。我加过几条规则效果不错。第一条是孤立节点检查。如果某个节点没有任何连线可能是忘了连也可能是废弃节点没删。第二条是循环依赖检查。如果 A 调用 BB 又调用 A可能是设计问题需要人工确认。第三条是命名规范检查。节点 ID 是否符合约定格式标签是否为空。这些检查用脚本就能做解析图源文件遍历节点和连线输出报告。接入 CI 后每次提交都会跑一遍问题早发现早修复。7. 我个人的实操体会这套 diagram-design 的玩法我用了两年多最大的感受是图的本质是逻辑不是美术。以前我花大量时间调图的美观度现在我把时间花在梳理逻辑上图反而更清晰了。因为逻辑对了布局引擎会自动帮你排好逻辑乱了再怎么调样式也是白搭。另一个体会是图源文件要当代码对待。该拆文件拆文件该写注释写注释该做评审做评审。我见过太多团队把图当附属品结果图越画越乱最后没人看。其实图是架构的镜子图乱了架构多半也乱了。最后分享一个小技巧每次架构评审前先让图源文件跑一遍渲染。如果渲染报错说明图源文件有问题先修图再评审。这个习惯帮我避免了好几次“评审时才发现图是错的”的尴尬。图对了讨论才有共同语言。