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.diagram:
service 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.theme:
theme 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.diagram:
template 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 负责保证产物最新。
比如文档里写:
CI 在构建文档前先渲染所有图,这样文档里的图永远是最新的。更进一步,可以在图源文件里加注释,渲染时把注释作为 tooltip,鼠标悬停显示详细信息。这样图既是概览,又是入口。
6.3 图的自动化检查
图也能做自动化检查,这一点很多人没想到。我加过几条规则,效果不错。
第一条是孤立节点检查。如果某个节点没有任何连线,可能是忘了连,也可能是废弃节点没删。第二条是循环依赖检查。如果 A 调用 B,B 又调用 A,可能是设计问题,需要人工确认。第三条是命名规范检查。节点 ID 是否符合约定格式,标签是否为空。
这些检查用脚本就能做,解析图源文件,遍历节点和连线,输出报告。接入 CI 后,每次提交都会跑一遍,问题早发现早修复。
7. 我个人的实操体会
这套 diagram-design 的玩法我用了两年多,最大的感受是:图的本质是逻辑,不是美术。以前我花大量时间调图的美观度,现在我把时间花在梳理逻辑上,图反而更清晰了。因为逻辑对了,布局引擎会自动帮你排好;逻辑乱了,再怎么调样式也是白搭。
另一个体会是,图源文件要当代码对待。该拆文件拆文件,该写注释写注释,该做评审做评审。我见过太多团队把图当附属品,结果图越画越乱,最后没人看。其实图是架构的镜子,图乱了,架构多半也乱了。
最后分享一个小技巧:每次架构评审前,先让图源文件跑一遍渲染。如果渲染报错,说明图源文件有问题,先修图再评审。这个习惯帮我避免了好几次“评审时才发现图是错的”的尴尬。图对了,讨论才有共同语言。