☰
用文本生成架构图:diagram-design 工具的设计与工程实践
2026/10/21 19:16:15 网站建设 项目流程

做技术方案的时候,你最不想碰的是什么?大概率是画架构图。手工用画图软件拖框连线,改一版需求图就废一半,组件一多连线跟蜘蛛网一样理不清,团队里每人一套画风,评审时光是“看懂图”就得花十分钟。为了解决这些破事,我做了个叫diagram-design的小项目,本质是一套用文本来描述和生成技术图表的工具集,目前已经在我自己参与的多个项目文档里跑稳了。这篇文章就围绕这个项目,把设计思路、关键实现、实际操作和踩坑记录完整拆一遍。适合被画图折磨过的开发、做技术方案的同学,也适合想在文档里嵌入可维护图表的人。

1. 项目整体设计与思路拆解

1.1 做这件事的起因:手工画图为什么这么痛

先说一个很实际的场景。去年我负责给一个内部系统做技术方案,需要画一张包含网关、若干微服务、数据库、消息队列的架构图。第一版我用通用画图工具拖了三个小时,看起来还挺像那么回事。结果方案评审时被问“加一个日志采集服务怎么放”,我心里就咯噔一下——这意味着所有相关连线、层级、颜色标注基本都要重来。

类似的情况反复出现多次之后,我认清了一个事实:架构图不是一次性产物,它是要跟着代码一起迭代的资产。但传统画图方式最大的问题在于,图的信息源头是人手拖拽出来的坐标,而不是结构化数据。坐标一旦固定,任何增删改都是一场灾难。

后来我留意到社区里不少团队开始用代码来画图,比如以文本形式描述节点和边,再通过引擎渲染成图片。这个思路让我豁然开朗:如果我们把图的本质看成“节点 + 边 + 属性”的数据结构,那么完全可以用一套DSL(领域专用语言)来描述这个结构,再把渲染交给专用引擎去处理。

1.2 整体思路:把“画图”变成“写代码”

diagram-design 的核心思路可以浓缩成一句话:一切皆文本,渲染靠引擎,样式走约定。

  • 一切皆文本:用户用一套简化语法描述图里的节点、连线、分组、标注,不碰任何坐标。
  • 渲染靠引擎:文本经过解析后,交给成熟的开源渲染后端生成 SVG、PNG 等格式。
  • 样式走约定:节点类型、颜色、线型都通过内置约定和可覆盖主题来统一,而不是让用户逐个去调。

选择这个路线,而不是直接封装某个可视化编辑器,背后有几个很实际的考虑。

第一,文本天然适合版本管理。架构图和代码一样进 Git,Review 的时候可以看到这个图在哪个版本发生了什么变化,甚至可以回滚。这是位图文件完全做不到的。

第二,文本适合程序化生成。团队里如果有几十个服务,手动画图不现实,但你完全可以写个脚本读取服务注册信息,自动生成一张服务依赖图。这个场景下,文本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.ddg:

title: 订单系统整体架构 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 SC"

macOS 可以用:

font: name: "PingFang SC"

Windows 下常见的是:

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加更多图标语义、接入更多渲染后端、把构建过程做成在线服务。但即使用最朴素的版本来组织团队的技术图产出,也远比回到手工拖框的时代舒服得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询