用文本生成架构图:diagram-design 工具的设计与工程实践
做技术方案的时候你最不想碰的是什么大概率是画架构图。手工用画图软件拖框连线改一版需求图就废一半组件一多连线跟蜘蛛网一样理不清团队里每人一套画风评审时光是“看懂图”就得花十分钟。为了解决这些破事我做了个叫diagram-design的小项目本质是一套用文本来描述和生成技术图表的工具集目前已经在我自己参与的多个项目文档里跑稳了。这篇文章就围绕这个项目把设计思路、关键实现、实际操作和踩坑记录完整拆一遍。适合被画图折磨过的开发、做技术方案的同学也适合想在文档里嵌入可维护图表的人。1. 项目整体设计与思路拆解1.1 做这件事的起因手工画图为什么这么痛先说一个很实际的场景。去年我负责给一个内部系统做技术方案需要画一张包含网关、若干微服务、数据库、消息队列的架构图。第一版我用通用画图工具拖了三个小时看起来还挺像那么回事。结果方案评审时被问“加一个日志采集服务怎么放”我心里就咯噔一下——这意味着所有相关连线、层级、颜色标注基本都要重来。类似的情况反复出现多次之后我认清了一个事实架构图不是一次性产物它是要跟着代码一起迭代的资产。但传统画图方式最大的问题在于图的信息源头是人手拖拽出来的坐标而不是结构化数据。坐标一旦固定任何增删改都是一场灾难。后来我留意到社区里不少团队开始用代码来画图比如以文本形式描述节点和边再通过引擎渲染成图片。这个思路让我豁然开朗如果我们把图的本质看成“节点 边 属性”的数据结构那么完全可以用一套DSL领域专用语言来描述这个结构再把渲染交给专用引擎去处理。1.2 整体思路把“画图”变成“写代码”diagram-design 的核心思路可以浓缩成一句话一切皆文本渲染靠引擎样式走约定。一切皆文本用户用一套简化语法描述图里的节点、连线、分组、标注不碰任何坐标。渲染靠引擎文本经过解析后交给成熟的开源渲染后端生成 SVG、PNG 等格式。样式走约定节点类型、颜色、线型都通过内置约定和可覆盖主题来统一而不是让用户逐个去调。选择这个路线而不是直接封装某个可视化编辑器背后有几个很实际的考虑。第一文本天然适合版本管理。架构图和代码一样进 GitReview 的时候可以看到这个图在哪个版本发生了什么变化甚至可以回滚。这是位图文件完全做不到的。第二文本适合程序化生成。团队里如果有几十个服务手动画图不现实但你完全可以写个脚本读取服务注册信息自动生成一张服务依赖图。这个场景下文本DSL是唯一的可行接口。第三文本降低了协作门槛。不懂设计工具的人也能上手写几行语法反过来团队里任何一个人改了一行文本所有人都能看懂改了什么。当然完全用代码画图也有短板比如精调布局的自由度不如鼠标拖拽。但我的定位很明确diagram-design 追求的是“快速、规范、可维护”而不是“像素级的自由创作”。技术方案图、系统架构图、时序图、网络拓扑图恰恰是这种定位的典型场景。1.3 关键技术选型与理由整个工具集的底座是一个轻量式DSL解析器渲染部分则选用了Graphviz作为主要后端。选它不是因为功能最多而是因为图形描述语言dot的生态最成熟文本转布局的能力足够稳定并且输出格式覆盖了SVG、PNG、PDF等文档场景常用的几种。备选方案我也考虑过D2、Mermaid、PlantUML。Mermaid在Markdown生态里很流行适合快速画流程图和时序图但复杂架构图的自定义能力偏弱D2语法更现代但生态和渲染细节不如Graphviz成熟。最终我采用了“自有DSL做入口 Graphviz做后端”的组合用户接触的是我给的一套简化语法不需要直接写原生dot底层的复杂度全部封装在工具里后续如果觉得Graphviz不够用我可以平滑地把渲染后端替换成其他引擎。这套架构带来的直接好处是使用方只依赖一种语法完全不需要了解渲染引擎的细节而我可以独立优化解析逻辑和渲染效果互不干扰。2. 核心细节解析与实操要点2.1 DSL语法设计的思路DSL的语法设计是项目里最花心思的部分。我的目标不是发明一套复杂语言而是让一个没接触过的开发者在30秒内看懂并写出一个像样的图。我用了一个简单的三元素模型节点node、边edge、分组group。节点用名称 [类型]表达类型不同会映射到不同的形状和颜色边用箭头连接分组用关键字group包裹。最基本的示例长这样gateway [service] order [service] mysql [database] gateway - order order - mysql这个片段定义了三件事三个节点分别属于服务类型和数据库类型两个箭头代表调用关系排版和坐标完全由引擎自动处理。用户不需要关心“网关画在左边还是右边”只需要表达“网关调用了订单订单访问数据库”。为什么要设计成这样而不是直接教用户写dot因为dot语法里光是有向图的边属性就有一大堆普通用户很容易迷失在细节里。我做的这层封装本质上是“最小可用子集”屏蔽引擎的复杂性只暴露用户真正关心的概念。语法设计过程中有一个关键决策类型决定样式。[service]、[database]、[cache]、[client]等内置类型会在渲染时自动对应一套预设颜色和图标约定。这样即便整个团队有几十人画图出来的风格也是统一的因为风格不依赖个人审美而是由类型系统强制保证。2.2 自动布局与分组的取舍布局是文本画图最容易翻车的地方。Graphviz的默认布局是有向分层算法它会自动决定节点上下层级和左右排列这对呈现调用链非常合适但遇到跨组连线多的图时也会乱。我做了两个层面的处理。第一个层面是“组”。group在渲染时会映射为Graphviz的subgraph引擎会把同一组内的节点尽量放在一起并用边框区分。典型用法是表示不同的服务单元或部署区域group 前端部署区 { web [service] h5 [service] } group 后端服务区 { gateway [service] order [service] }第二个层面是显式约束。写字的时候可以用方向属性来暗示布局倾向比如left to right这个指令告诉解析器整体按从左到右的流向排布。架构图里经典的阅读顺序是“客户端在左、后端在右、数据存储在最右”这个特性就把这类常见诉求直接变成了一个开关。经过大量的实际测试我发现一个规律图混乱的根源不是算法不行而是节点关系表达得不清晰。很多人画图喜欢把每两个节点之间都连一条线信息量很大但视觉噪声极大。合理做法是抽象出关键链路必要时用聚合线代替多对多连线。这部分我会在实操环节再展开。2.3 样式注入与主题系统样式是另一个容易失控的点。手工画图时每个人对“服务该用什么颜色”、“数据库该用什么形状”都有自己的想法。diagram-design 的做法是默认样式写死在类型系统里但提供主题覆盖机制。主题本质上是一个配置文件以结构化的形式声明不同元素的样式属性。比如要自定义数据库节点的填充色只需要写types: database: shape: cylinder fill: #F3E8FF stroke: #7C3AED解析器加载主题后渲染时会把对应属性合并进底层的节点定义。这套机制在实际使用中特别管用。比如某团队的公司VI主色是蓝色那么只需要出一份主题文件所有图都会自动切换成品牌色完全不用改任何图源文件。2.4 导出格式与文档集成导出管线我做得比较克制。当前支持 SVG、PNG 和 PDF 三种格式。SVG是默认选项因为它矢量无损、体积小、还可以直接在HTML文档中嵌入PNG用于文档预览和博客配图PDF用于正式方案书。做到文档集成这个环节时一个值得注意的细节是“预览图 源文件”的双轨策略。如果团队用Markdown写技术文档直接在文档里内嵌最终图片显然不便于更新更好的做法是约定文档引用的图片指向构建产物源文件则单独维护在同一个代码仓库里。每次图有改动跑一遍构建命令文档里的图就自动更新了。这已经在我们的技术文档体系里形成固定流程。3. 实操过程与核心环节实现3.1 环境准备与安装运行环境我用的是 Python 3.10依赖管理推荐用虚拟环境。安装那一步其实很省事pip install diagram-design装好之后还要确认渲染后端可用。因为渲染是调用 Graphviz 引擎完成的所以系统里需要存在对应程序。安装完可以做个验证ddg --version如果提示找不到后端多半是 Graphviz 没有安装或不在 PATH里解决办法是安装对应发行版或提供环境变量指向可执行文件路径。3.2 第一个完整示例订单系统架构图我拿一个模拟项目订单系统来演示完整实操。先在工作目录里建一个源文件order.ddgtitle: 订单系统整体架构 left to right group 客户端 { app [client, 标签: APP] web [client, 标签: H5] } group 接入层 { gateway [service, 标签: 网关] auth [service, 标签: 认证] } group 业务层 { order [service, 标签: 订单服务] stock [service, 标签: 库存服务] user [service, 标签: 用户服务] } group 数据层 { order_db [database, 标签: 订单库] stock_db [database, 标签: 库存库] cache [cache, 标签: 缓存] } app - gateway web - gateway gateway - auth gateway - order gateway - stock gateway - user order - order_db stock - stock_db order - cache stock - cache然后跑构建命令ddg build order.ddg -o order.svg执行完会在当前目录生成order.svg。如果你用的编辑器支持Markdown预览甚至可以直接把SVG拖进去看效果。这个例子最大的价值在于整个图从无到有用不到一分钟源文件只有不到30行但表达的信息量却足够一次方案评审使用。我特别喜欢这个流程的地方在于后续如果“接入层”要多加一个限流组件源文件里加一行节点、再加两条边就完了重新构建一遍十分钟前辛苦拖出来的图完全不比现在这个差。3.3 文档与 CI/CD 的集成实践在团队推广这套工具的时候我做得最多的一件事是把它嵌进文档自动构建流程。思路是画图源文件放在一个固定的diagrams/目录构建脚本负责把目录下所有.ddg文件全部渲染成 SVG再放到docs/images/供文档引用。脚本长这样for f in diagrams/*.ddg; do ddg build $f -o docs/images/$(basename $f .ddg).svg done如果配合 CI 流水线每次提交之后自动跑一遍基本可以保证文档里的架构图永远和源文件同步。这对后续维护是巨大的解脱。以前最怕的东西就是“文档里的图早就过时了但没人去重画”现在图是构建出来的过时意味着构建挂了或者有人偷懒没提交源文件问题很容易被追责和修复。3.4 自定义主题的实际操作主题文件可以让一套图在不同场景下快速换装。建立一个theme.yaml内容可以这样title: align: center font: 16px, bold types: client: shape: box fill: #E0F2FE stroke: #0369A1 service: shape: box3d fill: #FEF3C7 stroke: #B45309 database: shape: cylinder fill: #FCE7F3 stroke: #BE185D cache: shape: component fill: #D1FAE5 stroke: #047857构建时指定主题ddg build order.ddg -o order.svg --theme theme.yaml使用主题系统之后我经常干的一件事是“一键生成白底图”和“一键生成深色图”。技术Blog我要浅色配图幻灯片背景是深色的我就换深色主题源文件完全不动视觉效果差异却可以很大。这里的核心经验是颜色和形状的决策要跟图的内容解耦否则一旦品牌规范变了全量修改的成本会让你想重画。4. 常见问题与排查技巧实录4.1 布局混乱节点交叉、线条重叠文本画图最常见的翻车现场就是节点多之后布局乱成一锅粥。我测试过一个有个几十个节点的图默认渲染结果确实一言难尽大量跨组连线横穿图面看起来像是传输层的线路图完全没法用。排查思路是有层次的。首先看分组是否合理检查每个节点是否归属到了正确的组里其次检查节点之间的边是否过度冗余比如服务A调用了服务B和C但B和C又各自有调用链如果全画出来就是交叉重灾区最后再考虑显式布局方向。大多数情况删掉非必要的边、理清层级边界之后布局会自动恢复到可读状态。我特别想提醒的是别指望引擎替你解决所有布局问题重要的图要遵循“少边优先”的原则。能用聚合边表达的不要逐条画能分层的不要跨层直连。4.2 中文标签显示成方框Graphviz渲染中文偶尔会全是方框原因几乎都是字体缺失。不同的操作系统默认字体不一样但引擎未必认识它们。解决方案是在主题里显式声明中文字体。Linux 下常用方案font: name: Noto Sans CJK SCmacOS 可以用font: name: PingFang SCWindows 下常见的是font: name: Microsoft YaHei配好字体之后中文基本不会再出问题。我的建议是把字体声明直接写进团队的默认主题里否则每个新人第一次跑项目导图都会踩一遍这个坑。4.3 导出的图片模糊反馈模糊的基本都是用了 PNG 格式且分辨率不足。文本画图的底层先渲染成矢量图因此解决路径也很直接优先导出 SVG或者调高位图渲染的采样倍数。构建命令提供了一个缩放参数试过之后效果立竿见影ddg build order.ddg -o order.png --scale 2如果不希望文档里的图体积太大SVG 永远是首选。矢量图在网页上放大缩小都清晰体积通常也就几十KB。4.4 大图构建很慢甚至卡死有一次我生成一张包含上百个节点、几百条边的服务治理全景图构建命令跑了几分钟还没结束。原因也简单我是按照真实依赖关系全量生成的没有做任何裁剪和摘要。排查后发现最有效的手段是“分而治之”。大型图不要试图一次性画完按领域边界拆成多个子图每个子图独立构建、独立发布再在总览里用粗力度的箭头表达领域间关系而不是细化到每一个服务。这个过程其实跟代码里做模块拆分是同构的。如果确实需要全量展示务必检查是否有一对多、多对多的连边没有聚合这是图规模爆炸的主要推手。4.5 构建提示语法错误但找不到位置这个问题的核心是报错信息不够友好。后来我在解析器里专门加了“错误定位”功能报错会精确到文件第几行附近的片段。遇到这种情况经验是先检查节点名称有没有拼写不一致再检查中括号、分组大括号是否成对最后看有没有关键字写错。文本DSL的报错往往不是什么高端问题绝大多数是手滑导致的低级错误。4.6 快速排查速查表现象常见原因最快解法中文变方框缺少中文字体主题中显式配置字体名布局交叉严重边冗余、分组不合理清理多余边检查分组PNG模糊渲染分辨率不足换SVG或使用--scale 2构建很慢节点边数量过大拆分图聚合关系边语法检查报错名称不匹配、括号不配对看报错定位逐行排查组关系乱掉subgraph嵌套层级不符合预期检查group的闭合位置5. 在真实团队里沉淀出的几条使用建议5.1 规范命名让图源文件像代码一样可读这套工具用多了之后我最大的感受是图源文件的读者不只是渲染引擎还包括你的同事以及三个月后的你自己。因此节点命名要跟真实的模块名、服务名对齐不要图省事写缩写。标签属性可以展示更友好的名称而真正的ID应该保持稳定。举例来说如果服务注册中心里叫user-center节点就写成user_center [service, 标签: 用户中心]而不是写成uc。一旦团队规模变大命名混乱带来的阅读成本会直线上升。5.2 图源文件进 Git 仓库绑定版本这一点怎么强调都不过分。图源文件同代码一起提交意味着每次重构、每次新方案图的变化都是可以追溯的。遇到一个历史架构问题你可以直接查看当时的图源文件而不是翻图片聊天记录。有一次我们在排查一个接口调用链异常正是靠当时提交的架构图源文件快速定位到了引入的模块依赖关系变化这比靠记忆复盘高效得多。5.3 把重复画图逻辑脚本化如果你要画的图规模较大还是建议写脚本生成DSL而不是手写。比如从一个配置中心导出服务列表自动生成“服务依赖图”用代码拼接文本这比一行行手写稳定得多。diagram-design 的DSL让我能够把画图变成一种“数据投影”源数据的任何变化重建一次就映射到图上。5.4 离线环境下怎么救人有朋友问过在完全隔离的内网环境里能不能用这套工具。答案是可以的只要把运行环境、依赖包、渲染后端一次性装好剩下的全流程都在本地完成不依赖外部网络服务。这点对于需要离线文档评审的场景尤其友好。6. 我给这套流程设计的最后一块拼图如果团队要立刻上手这套流程我会推荐一个最小落地的组合装好工具建一个diagrams/源文件目录写一份默认主题把源文件目录挂进CI。三十分钟内你就能看到团队里第一张文本架构图出现在方案文档里。我在实际推进这个项目时遇到的最大阻力从来不是工具不好用而是团队成员对“画图居然还能这么干”的惯性怀疑。但通常只要让他们亲手改一次源文件并重新生成图片这个疑虑就会消散。对我来说diagram-design 最大的回报是以后任何一次方案讨论中当有人提出“加个组件/改条链路”时我再也不会有画图恐惧而是自然地打开源文件加一行节点、补两条边重新构建把最新架构图投到屏幕上。如果哪天你想扩展这套流程可以顺着这几个方向试试给DSL加更多图标语义、接入更多渲染后端、把构建过程做成在线服务。但即使用最朴素的版本来组织团队的技术图产出也远比回到手工拖框的时代舒服得多。