1. 从“diagram-design”这个标题能读出什么
第一次看到“diagram-design”这个标题,很多人会以为它只是某个绘图工具的别名,或者一个泛泛的设计类话题。但如果你在技术社区待过一段时间,就会发现这类标题背后往往藏着一个非常具体的需求:用代码或结构化文本生成图表,而不是靠鼠标拖拽。这个需求在过去几年里增长得非常快,原因也很直接——当系统架构、数据流程、状态机、部署拓扑这些东西需要频繁更新时,手工拖拽的图几乎必然会在某个时间点变得过时,而且没人愿意去维护它。
“diagram-design”这个标题的核心,其实是在问一个问题:图表的“设计”能不能像写代码一样被管理?这里的“设计”不是指配色和排版的美学设计,而是指图表的语义结构设计——节点怎么划分、边怎么连接、层级怎么组织、布局怎么约束。一旦你把图表当成一种数据结构来看待,很多问题就变得可编程了:版本控制、差异对比、自动生成、批量渲染、CI 集成,这些在传统绘图工具里很难做到的事情,在“图表即代码”的范式下都变得顺理成章。
这篇文章适合几类人看:第一类是经常需要画架构图、流程图、时序图的后端或全栈开发者;第二类是技术文档工程师,需要维护大量图表且希望它们和文档一起版本化;第三类是对“图表即代码”这个方向感兴趣、想找一个可落地工具链的人。我会围绕 diagram-design 这个主题,把工具选型、语法设计、布局控制、渲染管线、协作维护这几个层面拆开来讲,尽量让你看完之后能直接搭出一套自己的图表工作流。
需要提前说明的是,下面提到的具体工具和语法都是基于我在实际项目中的使用经验,不同团队可以根据自己的技术栈做替换。核心思路是一致的:把图表的语义和渲染分离,让图表可以被程序处理。
2. 为什么“图表即代码”值得你花时间折腾
2.1 手工绘图的隐性成本比你想的高
很多人觉得画图嘛,拖几个框、连几条线,十分钟搞定。但真正做过长期维护的人都知道,手工绘图的成本不在第一次画,而在第 N 次改。一个典型的场景是:你画了一张系统架构图,里面有十几个服务节点。三个月后,其中一个服务拆成了两个,另一个服务换了名字,还有一个新增的缓存层。这时候你要打开原来的绘图文件,找到对应的框,改文字、调位置、重新连线,还要保证整体布局不乱。如果这张图还在多个文档里被引用,那工作量直接翻倍。
更麻烦的是,手工绘图工具的文件格式通常是二进制或者私有 XML,你没法用 git diff 看出“这次改了什么”。评审的时候只能靠人眼对比两张图,效率极低。而“图表即代码”把图表的定义变成纯文本,每一次修改都是一次可读的 diff,评审、回滚、追溯都变得和代码一样自然。
2.2 结构化文本让图表可以被程序消费
把图表写成结构化文本之后,一个额外的好处是:同一份图表定义可以渲染成多种输出。比如你定义了一张部署拓扑图,它可以被渲染成 PNG 放进文档,可以被渲染成 SVG 嵌进网页,甚至可以被解析成 JSON 供其他系统消费。这种“一次定义、多处使用”的能力,在手工绘图工具里几乎是不可能实现的。
我在一个项目里就遇到过这样的需求:运维团队需要一张网络拓扑图放在监控大屏上,开发团队需要同一张图放在技术文档里,而自动化脚本需要从图里提取节点关系做健康检查。如果手工画三份,维护成本不可想象;但用图表即代码的方式,只需要维护一份源文件,三个消费方各自渲染即可。
2.3 版本控制与协作的自然契合
代码协作的那一套流程——分支、合并、评审、回滚——可以直接套用到图表上。你可以为一个架构调整开一个分支,改完图表定义后发起合并请求,评审人看到的是清晰的文本 diff,而不是两张模糊的截图。合并之后,CI 可以自动渲染出最新的图并发布到文档站点。整条链路是自动化的,人只需要关注语义变更本身。
提示:如果你的团队已经在用 git 管理文档,那么把图表源文件放进同一个仓库是最省事的做法。图表和它描述的代码放在一起,改代码的时候顺手改图,避免图和实现脱节。
3. 主流图表即代码方案的横向对比
3.1 几种常见路线的定位差异
在 diagram-design 这个领域里,目前有几条比较主流的技术路线,它们各自的定位和适用场景差别很大,选错了会很难受。我按“语法风格”和“渲染方式”两个维度来梳理一下。
| 方案类型 | 典型代表 | 语法特点 | 适合场景 | 学习曲线 |
|---|---|---|---|---|
| 声明式 DSL | 类 Mermaid 风格 | 接近自然语言,上手快 | 流程图、时序图、简单架构图 | 低 |
| 编程式绘图 | 基于代码库调用 | 用编程语言描述图形 | 复杂布局、数据驱动图表 | 中高 |
| 文本转图形 | 基于 DOT 语法 | 描述节点和边的关系 | 依赖图、状态机、网络拓扑 | 中 |
| 结构化标记 | 基于 XML/JSON | 描述图形元素和属性 | 需要精确控制的矢量图 | 中 |
声明式 DSL 的最大优势是上手快,你几乎不需要学什么新概念,写几行类似自然语言的描述就能出图。但它的缺点是布局控制能力弱,遇到复杂图的时候,自动布局经常给出你不想要的结果,而且很难微调。编程式绘图的优势是灵活,你可以用循环、条件、函数来生成图形,适合数据驱动的场景,但代价是你得写真正的代码,调试成本更高。
3.2 选型时最容易忽略的三个问题
第一个问题是渲染环境。有些方案依赖浏览器渲染,有些依赖本地运行时,有些需要服务端支持。如果你的图表要在 CI 里自动生成,就必须选一个能在无头环境里跑通的方案。我见过有人选了一个只能在桌面编辑器里预览的工具,结果到了自动化环节完全没法用,只能推倒重来。
第二个问题是布局算法的可控性。自动布局很美好,但当你需要一张“从左到右、分层清晰、关键节点居中”的图时,自动布局往往会给你一个“所有节点挤在一起”的结果。这时候你需要的是能够手动干预布局的方案,比如指定层级、指定方向、指定节点分组。选型时一定要拿一张你实际工作中最复杂的图去试,看能不能调出满意的效果。
第三个问题是输出格式的丰富度。PNG 适合文档,SVG 适合网页,PDF 适合打印。有些方案只支持一种输出,有些支持多种。如果你有跨媒介发布的需求,这一点必须提前确认。
3.3 我的实际选型思路
我在不同项目里用过好几种方案,最后形成的选型思路是这样的:先用声明式 DSL 快速验证,遇到布局瓶颈再切换到编程式方案。具体来说,如果一张图的节点数在 20 个以内、关系不复杂,声明式 DSL 完全够用,写起来快、改起来也快。但如果节点超过 30 个、有多层嵌套或者需要数据驱动,我就会直接用编程式绘图,虽然前期投入大一点,但后期的可维护性完全不是一个量级。
另外,我强烈建议在选型阶段就考虑图表的复用性。比如你有没有一些“通用组件”会在多张图里出现?如果有,那么支持“子图引用”或“模板复用”的方案会省你很多事。这一点在声明式 DSL 里往往支持得不好,而在编程式方案里可以很自然地用函数封装。
4. 用结构化文本描述一张图的完整思路
4.1 先定义语义,再考虑样式
很多人写图表定义的时候,习惯一边写节点一边调颜色和位置,结果写到一半发现结构不对,又要推倒重来。我的建议是分两步走:第一步只定义语义,也就是有哪些节点、它们之间是什么关系、分几个层级;第二步再考虑样式,也就是颜色、字体、间距、对齐。
举个例子,假设你要画一张“用户请求经过网关、鉴权、业务服务、数据库”的流程图。第一步你只需要写清楚:用户 -> 网关 -> 鉴权 -> 业务服务 -> 数据库,以及鉴权失败时走哪条分支。这时候你完全不关心节点画成方的还是圆的、什么颜色。等语义确认无误之后,再统一加样式。这样做的好处是,当结构需要调整时,你不会被样式细节干扰。
4.2 节点与边的命名规范
在结构化文本里,节点和边的命名直接决定了图的可读性和可维护性。我踩过的坑是:一开始用中文名当节点 ID,结果在某些渲染器里出现编码问题;后来又用纯数字 ID,结果过两个月自己都看不懂哪个是哪个。最后我固定了一套规范:节点 ID 用英文小写加下划线,显示名称用中文,两者分开定义。
比如:
node gateway "API 网关" node auth "鉴权服务" node biz "业务服务" node db "主数据库" gateway -> auth auth -> biz biz -> db这样 ID 稳定、显示名称可读,改显示名称不影响引用关系。边也可以加标签,比如auth -> biz : 鉴权通过,这样图上的连线会带上说明文字,评审的时候一目了然。
4.3 分组与层级的表达方式
复杂系统图如果没有分组,会变成一团乱麻。结构化文本里表达分组通常有两种方式:一种是子图(subgraph),把相关节点包在一起;另一种是层级(rank),指定哪些节点在同一层。子图适合表达“这些节点属于同一个模块”,层级适合表达“这些节点在同一个处理阶段”。
我通常会把两者结合使用:先用子图把模块划分出来,再用层级控制每个模块内部的节点排列。比如一个典型的微服务架构图,我会把“接入层”“服务层”“存储层”分成三个子图,然后让每个子图内部的节点从左到右排列。这样出来的图既有模块边界,又有清晰的流向。
注意:不同渲染器对子图和层级的支持程度不一样。有些渲染器会把子图渲染成一个带边框的矩形,有些则只是逻辑分组不显示边框。选型时要确认你用的渲染器支持哪种表达。
4.4 注释与文档化
图表定义文件本身也应该有注释。我习惯在文件开头写一段说明,讲清楚这张图描述的是什么系统、最后更新时间、维护人是谁。在关键节点旁边也会加注释,解释为什么这个节点存在、它和哪些外部系统有交互。这些注释不会出现在渲染结果里,但对后来维护的人(包括几个月后的你自己)非常有价值。
# 系统:订单处理链路 # 更新:2024-06 # 维护:后端架构组 # 说明:展示订单从创建到落库的完整路径 node order_api "订单接口" # 对外暴露的 HTTP 接口 node order_svc "订单服务" # 核心业务逻辑 node order_db "订单库" # 分库分表后的主库这种注释习惯看起来不起眼,但在团队协作里能省掉大量“这张图是干嘛的”这类沟通成本。
5. 布局控制的实战技巧与常见坑
5.1 自动布局为什么总是不听话
自动布局算法的目标通常是“减少边的交叉”和“均匀分布节点”,但它并不理解你的业务语义。比如它不知道“数据库应该放在最右边”,也不知道“这两个服务应该挨在一起”。所以自动布局出来的图,往往在语义上是正确的,但在视觉上是不符合直觉的。
我的应对策略是:先用自动布局出一版,然后针对性地加约束。大多数工具都支持一些基本的布局约束,比如指定方向(从上到下、从左到右)、指定节点层级、指定节点顺序。加约束的原则是“最小干预”——只在你觉得别扭的地方加,不要试图完全控制每一个节点的位置,否则你还不如去用手工绘图工具。
5.2 方向与层级的配合
方向(direction)和层级(rank)是布局控制里最常用的两个手段。方向决定了整体的流向,层级决定了节点在流向中的位置。比如一张从左到右的流程图,你可以指定某些节点在同一个层级,这样它们会垂直排列;指定某些节点在不同层级,它们会水平排列。
一个常见的坑是:方向和层级冲突。比如你指定了从左到右的方向,但又把两个应该上下排列的节点放到了同一层级,结果渲染器不知道该听谁的,出来的图会很奇怪。我的经验是,先确定整体方向,然后在局部用层级做微调,不要反过来。
5.3 节点间距与画布尺寸
节点间距和画布尺寸看起来是小事,但对图的可读性影响很大。间距太小,节点挤在一起,连线看不清;间距太大,图变得很稀疏,一屏放不下。大多数工具都有默认间距,但默认值往往偏小。我通常会把节点间距调到默认值的 1.5 倍左右,这样图看起来更透气。
画布尺寸方面,如果你的图要放进文档,建议控制在一页能放下的范围内。如果节点太多放不下,考虑拆成多张图,而不是硬塞进一张。一张信息密度过高的图,读者往往抓不住重点。
5.4 我踩过的三个布局坑
第一个坑是过度依赖自动布局。早期我总觉得自动布局很智能,结果每次出来的图都不一样,团队里每个人渲染出来的效果都有细微差异。后来我固定了布局参数,并且把渲染命令写进脚本,保证任何人执行都得到一样的结果。
第二个坑是忽略字体和编码。有一次图里的中文全部变成了方块,排查半天发现是渲染环境缺少中文字体。从那以后,我在 CI 里都会显式安装所需字体,并且在文档里注明字体依赖。
第三个坑是没有版本锁定。图表工具的版本升级有时会改变默认布局算法,导致同一份源文件在不同版本下渲染出不同的图。后来我在项目里锁定了工具版本,升级时先在一张测试图上验证效果,确认没问题再全量升级。
6. 把图表接入文档与自动化流程
6.1 图表源文件应该放在哪里
我的做法是图表源文件和它描述的代码放在同一个仓库,通常放在docs/diagrams/目录下。这样做的好处是:改代码的时候顺手改图,评审代码的时候一起评审图,避免图和实现脱节。如果图表是跨仓库的全局架构图,那就单独建一个文档仓库,但要在各个代码仓库里放链接指向它。
目录结构可以这样组织:
docs/ diagrams/ architecture/ overview.diagram order-flow.diagram deployment/ prod-topology.diagram README.md每个子目录放一类图,README 里说明这个目录下的图分别描述什么、什么时候更新。
6.2 在 CI 里自动渲染
把渲染命令写进 CI 脚本,每次合并到主分支时自动重新生成图片并发布。这样文档站点上的图永远是最新的,不需要人工干预。一个典型的 CI 步骤大概是:安装渲染工具 -> 执行渲染命令 -> 把输出图片复制到文档目录 -> 提交或发布。
需要注意的是,渲染工具本身也要版本锁定,否则某天工具升级导致渲染结果变化,你会收到一堆莫名其妙的图片 diff。我通常会在项目里放一个requirements.txt或package.json来锁定版本。
6.3 文档中的引用方式
在 Markdown 文档里引用图表,我推荐用图片链接而不是内嵌源码。也就是说,文档里写,而不是把图表源码直接贴进文档。这样文档保持干净,图表源码单独维护,各司其职。
如果文档平台支持,还可以在图片下面加一个“查看源码”的链接,指向对应的.diagram文件。这样感兴趣的读者可以去看图表是怎么定义的,也方便其他人修改。
6.4 评审流程的调整
引入图表即代码之后,评审流程需要做一点调整。以前评审图是看截图,现在评审图是看文本 diff。评审人需要关注的是:节点增删是否合理、关系变更是否符合预期、分组和层级是否还清晰。我建议在合并请求的描述里,除了代码变更说明,也加一段“图表变更说明”,讲清楚这次改了哪些节点、为什么改。
提示:如果团队里有人不熟悉图表源码,可以在合并请求里附上渲染后的图片,这样所有人都能看懂变更效果。文本 diff 给熟悉的人看,图片给不熟悉的人看,两者互补。
7. 团队协作中的维护经验
7.1 谁来维护图表
图表维护最容易出现的问题是“三不管”——开发觉得是文档团队的事,文档团队觉得是开发的事,最后没人管。我的经验是明确一个 owner,通常是对应模块的开发者。谁改了这个模块的代码,谁就负责更新对应的图。文档团队负责审核图的表达是否清晰,但不负责内容正确性。
对于全局架构图,可以指定一个架构组的成员作为 owner,定期(比如每季度)review 一次,确保图和实际架构一致。
7.2 图表腐化的预防
图表腐化是指图还在,但描述的内容已经和实际不符。预防腐化的关键是把图表更新纳入开发流程。比如在代码评审清单里加一条:“如果本次变更影响了系统结构,是否更新了对应的图表?”这样每次结构变更都会被提醒。
另一个手段是定期自动检查。有些工具可以从代码里提取服务依赖关系,和图表定义做对比,发现不一致就报警。虽然这种检查不能覆盖所有情况,但能抓住大部分明显的腐化。
7.3 新人上手图表工具的成本
图表即代码的工具通常需要一点学习成本,尤其是编程式方案。我的做法是在仓库里放一份简短的贡献指南,讲清楚图表文件放在哪、怎么渲染、怎么提交。再配一两个简单的示例文件,新人照着改就能上手。不要指望新人自己去读工具的完整文档,那太耗时了。
另外,我会在团队内部做一次 30 分钟的分享,现场演示从改图表源码到渲染出图的全过程。这种一次性的投入,能省掉后面无数次的重复答疑。
7.4 跨团队协作的注意事项
如果多个团队共用一套图表工具链,需要提前约定好工具版本、渲染参数、输出格式。我见过两个团队各自用不同版本的同一工具,结果同一份源文件渲染出来的图不一样,合并的时候产生冲突。解决办法是建一个共享的配置仓库,所有团队都从这里拉取工具配置,保证环境一致。
还有就是命名冲突。不同团队可能给节点起了相同的 ID,合并图表的时候会冲突。建议在节点 ID 里加上团队前缀,比如order_svc、pay_svc,避免歧义。
8. 一些实际项目中的体会
我在几个项目里推行过图表即代码的工作流,最大的体会是:这件事的技术难度不高,难的是习惯的转变。一开始大家都不太愿意写文本定义,觉得不如拖拽来得快。但坚持两三个月之后,当第一次需要回滚一张图、第一次用 diff 评审图表变更、第一次在 CI 里自动出图的时候,大家就体会到好处了。
另一个体会是不要追求一步到位。一开始可以只把最常变的几张图改成代码定义,其他的先保持手工。等团队熟悉了流程,再逐步扩大范围。一上来就要求所有图都代码化,往往会遇到很大的阻力。
最后分享一个小技巧:如果你不确定某个图表工具是否适合你的场景,可以拿一张你实际工作中最复杂的图去试。花一个小时把这张图用目标工具写出来,看看渲染效果、调整难度、输出质量。这一个小时的投入,比看十篇评测文章都有用。工具好不好,只有拿真实场景试过才知道。