图表即代码:用结构化文本实现可版本控制的图表设计 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 文档里引用图表我推荐用图片链接而不是内嵌源码。也就是说文档里写![架构图](diagrams/architecture/overview.png)而不是把图表源码直接贴进文档。这样文档保持干净图表源码单独维护各司其职。如果文档平台支持还可以在图片下面加一个“查看源码”的链接指向对应的.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 里自动出图的时候大家就体会到好处了。另一个体会是不要追求一步到位。一开始可以只把最常变的几张图改成代码定义其他的先保持手工。等团队熟悉了流程再逐步扩大范围。一上来就要求所有图都代码化往往会遇到很大的阻力。最后分享一个小技巧如果你不确定某个图表工具是否适合你的场景可以拿一张你实际工作中最复杂的图去试。花一个小时把这张图用目标工具写出来看看渲染效果、调整难度、输出质量。这一个小时的投入比看十篇评测文章都有用。工具好不好只有拿真实场景试过才知道。