diagram-design 这个名字懂行的人一眼就能看出来它不是单纯的美工画图而是一整套把图表当代码来管理的工程化方案。我在实际项目里折腾这套东西也快两年了从最开始用 Visio 画完图没人更新到后来团队所有人都在一个仓库里维护架构图感受很深。今天就把这套 “图床即代码库” 的完整实践拆开讲讲包括我踩过的坑、工具选型的纠结、以及最终沉淀下来的工作流。如果你也在为技术文档里的图片腐烂、流程图改了十版没人同步、或者多人协作时图表冲突而头疼这篇文章应该能给你一套可以直接抄作业的解法。1. 为什么我最终放弃了拖拽式画图工具先交代一下背景。我所在的团队维护着一个中等规模的后端系统模块数量接近 20 个上下游依赖复杂每次做架构评审都得临时画图画完就扔。后来文档里沉淀了一批架构图但半年后系统接口改了三四轮文档里的图早就和代码对不上了新同学照着图排查问题直接查错方向。痛定思痛我决定寻找一种能让图表“活”起来的方案于是锁定了 diagram-design 这个方向图表用纯文本定义存放在代码仓库里参与版本管理、Code Review、自动校验和代码一起演进。拖拽式工具的痛点其实不只是“没人更新”这么简单。Visio、ProcessOn 这类工具的图形存储在二进制文件里无法 diff两个人同时改一张图合并几乎不可能而且图是“画”出来的节点坐标、连线角度都是美术操作想从一张图里提取结构化数据或者做自动化校验根本没有入口。最致命的是当系统规模上来之后一张 100 个节点的架构图在画布上手动排版时间和精力成本高得离谱。“图表即代码”解决的就是这三件事可版本化、可协作、可自动化。你把图表的逻辑结构写在一个 .puml 或者 .mmd 文件里它本质上是文本Git 能精确地告诉你这一行改动影响了哪个节点、哪条边。Code Review 的时候同事不用打开画图工具直接看代码就知道你改了哪里。加上 CI 里的语法校验和渲染输出图表永远不会因为“忘了重新导出图片”而和源文件脱节。有人可能觉得代码化图表的学习成本高不如拖拽工具直观。我的经验是这个说法只对画一次性临时图成立。凡是需要长期维护、多人共享、频繁变更的图表代码化的学习成本会在第一次真正需要“改图”的时候收回成本。你只需要记几个基础语法剩下的交给工具。2. 工具选型Mermaid、PlantUML 与 Graphviz 的取舍2.1 三种主流方案的定位差异diagram-design 这个方向社区里最成熟的三个选手是 Mermaid、PlantUML 和 Graphviz。它们不是同一个层面的东西很多人选型时混在一起比较其实是没搞明白各自的定位。Mermaid主打“轻量、易读、JS 生态”。语法像 Markdown 一样简洁浏览器里直接渲染非常适合嵌入博客、Wiki、项目 README。它擅长流程图、时序图、甘特图、状态图这类“业务视角”的图表。PlantUML主打“严谨、UML 标准”。它对 UML 规范的支持最完整时序图、用例图、组件图的表达力很强而且能生成 SVG、PNG、ASCII art 多种格式。老牌 Java 生态里用得最多。Graphviz严格说它不是“画图工具”而是一个布局引擎。它用 DOT 语言描述图结构布局算法如 dot、neato、fdp非常强大适合节点多、关系复杂的图比如依赖关系图、网络拓扑图。我最终的选择是“Mermaid 为主Graphviz 为辅”。Mermaid 负责绝大部分文档配图因为它对 Markdown 生态的融合度最好我们团队的文档平台支持 Mermaid 语法直接渲染写文档时右键粘贴图片的做法彻底退役了。Graphviz 则用来处理那些节点超过 50 个、需要稳定布局的复杂依赖图这种场景下 Mermaid 的自动布局会乱到没法看。2.2 一个容易忽视的选型因素渲染依赖很多人对比这几个工具时只看语法和输出格式却忽略了渲染环境。PlantUML 官方推荐的方式是跑本地 jar 包或者服务器它内部要调用 Graphviz 做布局计算所以想在 CI 里渲染 PlantUML 图得先保证构建环境里装好了 Graphviz 的二进制。Mermaid 就好很多它纯 JavaScript 实现Node 环境里npm install 一下就能跑也可以直接用 mermaid-cli 在命令行渲染。如果团队用 GitLab CI 或者 GitHub ActionsMermaid 的接入成本几乎是零。我踩过的一个坑是团队文档平台自带 Mermaid 渲染但版本比较老不支持较新的语法特性比如 block 分支的语法。我画的时候本地用最新版 mermaid-cli 渲染没问题提交到平台后直接空白。解决方案很简单把渲染结果制作为 SVG 文件存到仓库文档里引用 SVG 而不是直接嵌入平台渲染。虽然这又回到了“图文件入库”但至少源文件是文本SVG 只是构建产物能通过 CI 自动生成不会过期。2.3 我的推荐组合和工作流如果你要从零搭一套 diagram-design 流程我建议直接照抄这套组合图表源文件统一放在仓库的 diagrams 目录按类型分子目录比如architecture/、sequence/、flow/源文件采用.mmdMermaid格式特殊场景用.dotGraphviz写一个 Makefile 或者 shell 脚本调用 mermaid-cli 批量渲染所有.mmd文件到docs/diagrams/目录输出 SVGCI 中增加一个 job运行渲染脚本和语法校验如果图表文件有语法错误或者渲染产物与源文件不一致就阻断合并请求文档中统一引用docs/diagrams/下的 SVG 文件不直接在文档里写图表代码这套流程跑起来之后团队任何人都可以改图、提 PRmaintainer 审查的是文本差异渲染产物由 CI 自动更新任何人都不需要手动导出图片。后续我会详细说每一步的实现。3. 核心设计如何组织 chart-as-code 项目结构3.1 目录结构规划的底层逻辑diagram-design 项目最先要考虑的不是怎么写图而是图和代码怎么共处。我个人推荐的目录结构是这样的repo/ ├── diagrams/ │ ├── README.md │ ├── architecture/ │ │ ├── overview.mmd │ │ ├── payment-service.mmd │ │ └──>%% 订单流程时序图 %% 变更 %% - 2024-05-20 关闭预授权改为直接扣款 %% - 2024-08-01 新增风控异步校验节点这段注释看起来不起眼但在多人协作时价值很大。当 CI 渲染出的图和预期不符时第一件事就是看变更记录比翻 Git log 快得多。3.3 主题样式的统一管理画图最容易被忽视的就是样式统一。三五张图看不出问题当几十张图放在一起时不同颜色、不同线宽的节点会让人瞬间感到混乱。Mermaid 支持通过%%{init: {...}}%%指令初始化主题我把它抽到了一个公共文件里每张图引用同一套主题参数。常见的做法是这样在每张图的开头写 init 指令指定主题和颜色。我的建议是把主题参数写进一个单独的文件比如 shared/theme.json渲染脚本里通过 --configFile 参数加载这样源文件里不需要重复写一大段初始化代码也让团队换肤时只需要改一处。实际上mermaid-cli 是支持-c参数指定配置文件的我们可以把字体、颜色、布局间距都定义在配置文件里让所有的图保持视觉一致性。4. 实操篇Mermaid 绘图的几个关键语法和踩坑点4.1 流程图别把所有节点画成同一个形状Mermaid 的流程图语法很简单graph TD后面跟着节点和连线。但“能画出来”和“画得清楚”是两回事。我在评审别人图的时候最常见的问题是所有节点全是方框所有连线全是普通箭头阅读者根本分不清动作和状态。我的习惯是给节点分类开始/结束用圆角矩形A[开始]业务动作/系统调用用普通矩形B[创建订单]判断/分支用菱形C{余额充足?}外部系统/数据源用圆柱体D[(数据库)]这样读者扫一眼图形就能知道哪些是动作哪些是条件分支哪些是外部依赖完全不需要看文字。这比任何冗长的描述都高效。4.2 时序图复杂消息一定要用序号和注释时序图是 Mermaid 里我用得最多的类型因为它特别适合描述接口调用链和异步消息流。Mermaid 的时序图语法核心是参与者声明和消息箭头。我最推荐的细节是给关键消息行加上注释说明这是一个同步调用还是异步回调超时时间是多少失败后走什么分支。这些信息画在代码注释里比写在文档正文里更直观。一个典型的时序图代码示例注意这是 Mermaid 源文件的写法sequenceDiagram participant U as 用户 participant A as 订单服务 participant B as 支付服务 participant C as 风控服务 U-A: 提交订单 A-C: 异步风控校验(订单ID) alt 风控通过 A-B: 创建支付单 B--A: 支付二维码 A--U: 返回支付信息 else 风控拒绝 A--U: 提示风控拦截 end注意alt/else这种异步分支的用法它比把分支画成流程图更贴合业务语义。还有一个常用的是Note over给某个参与者加说明比如标注“超时时间 3s”很多老工程师看时序图会下意识找超时信息。4.3 状态图与甘特图高频但容易被忽略的两个场景除流程图和时序图外状态图和甘特图是另外两个高频场景但很多人画得一言难尽。状态图stateDiagram-v2在描述订单状态机时非常清晰语法上注意状态切换用--状态内部可以嵌套state 名称 { ... }。用状态图代替文字描述状态机可以减少大量“如果是 XX 状态那么 YY 状态……”之类的口语化文档。甘特图gantt则适合做项目排期。Mermaid 甘特图的核心是理解日期格式和section分组。有一点特别容易踩坑日期格式默认是 YYYY-MM-DD但如果加上axisFormat %m-%d配置可以让横轴更易读。甘特图虽然不能替代专业的项目管理工具但放在技术方案文档里给个小排期示意是很有用的。4.4 渲染脚本的具体实现说一千道一万图最终要渲染成图片才有价值。我的渲染脚本核心思路是用 mermaid-cli 的mmdc命令遍历所有.mmd文件逐一生成 SVG。这里贴一个简化版脚本#!/usr/bin/env bash set -euo pipefail OUTPUT_DIRdocs/diagrams CONFIG_FILEdiagrams/shared/theme.json mkdir -p $OUTPUT_DIR for file in diagrams/**/*.mmd; do name$(basename $file .mmd) echo Rendering $name... npx mermaid-js/mermaid-cli \ --input $file \ --output ${OUTPUT_DIR}/${name}.svg \ --configFile $CONFIG_FILE \ --scale 2 done脚本里的细节有几个--scale 2是为了导出高分辨率的图像因为很多文档平台会压缩 SVG如果不做缩放网页上会显得发虚。--configFile指向公共主题配置保证所有图风格一致。set -euo pipefail是为了让任何一步失败都能中断 CI不落入“图坏了但流程照常通过”的尴尬。CI 的接法很简单以 GitHub Actions 为例加一步拉取代码、装 Node、跑脚本然后用 git diff 检查是否有未提交的变更如果有就 fail 这个 job提醒提交者把渲染产物一并更新。这个“产物是否最新”的检查很关键它防止了源文件和 SVG 失同步。5. 版本管理与协作把图表当成代码来 Review5.1 diff 的可读性取决于你怎么写节点文本图表参与 Code Review 后最难办的事情就是改动很大但 diff 上看不出来逻辑变化。原因是很多人把多个信息塞进一个节点文本里比如A[创建订单br/调用支付br/校验库存]一旦这一行文字变了Git diff 会提示一整行修改审查者需要仔细比对才看得出哪里变了。更好的做法是一个节点只表达一个动作多个动作拆成多个节点用边连接。这样改一个动作时只动一个节点diff 一目了然。另外文本内部换行用br/会导致 diff 行很长我一般直接用/分隔或拆成多个节点。Mermaid 支持在同一个节点文本里用br/但这是“能画”和“好维护”的区别既然目标是把图表当代码维护节点之间耦合度越高未来的维护成本就越高。5.2 图表的命名空间和复用当团队里图表多了会发现同一套组件被反复画。比如“Redis 集群”这个节点可能在十张图里都出现。这时候有两种选择一是各画各的简单但后续要升级这套组件的样式时得改十张图二是用 Mermaid 的!include能力部分工具链支持或者在渲染时做文本拼接把公共模板抽出来。我的经验是前几年各画各的还好但超过 20 张图之后还是值得抽公共组件。具体做法你可以在渲染脚本里做一个简单的宏替换或者用稳定版本的 Mermaid 语法把公共子图放在源文件里。核心思想是图表的可维护性比画面上的美观更重要否则图表只能是文档的一次性装饰。5.3 Review 检查清单我给自己和团队定了一个 Checklist每次图表 PR 都会逐条过图是否与代码实现一致架构图对应服务发现配置时序图对应核心业务方法调用是否缺少边界条件比如异常分支、超时处理节点命名是否统一有没有同一个实体出现两种叫法渲染产物 SVG 是否已同步更新是否使用了团队统一的主题配置颜色有没有偏离这个清单让图表 Review 变成了机械式检查不容易漏项也方便新人上手。在推进 diagram-design 初期这是提高团队接受度的关键动作——因为大家都不愿意在没有标准的流程里消耗额外精力。6. 常见问题与排查技巧实录6.1 中文乱码和字体问题Mermaid 渲染中文通常没问题但如果你用 mermaid-cli 在 Linux 服务器上渲染有可能会遇到字体缺失导致的豆腐块或者乱码。解决方法是在配置文件的 fontFamily 里显式指定一个服务器上存在的中文字体比如Noto Sans CJK SC、WenQuanYi Micro Hei。如果服务器上没有这些字体就得先安装字体包或者在 Docker 基础镜像里带上。个人建议把你常用的中文字体直接打进构建镜像里一劳永逸。6.2 Mermaid 语法升级导致的渲染失败Mermaid 迭代速度非常快新版本经常调整语法解析规则。最难受的是团队文档平台内嵌的 Mermaid 版本比较老而本地用的是最新版。规避方法我前面说过不依赖文档平台渲染而是自己渲染 SVG 入库。还有一点如果你想锁版本可以把mermaid-js/mermaid-cli的版本号写死在 package.json 里并且定期手动升级、跑一遍全量渲染测试。很多团队嫌麻烦不做结果某次 upgrade 之后突然几十张图渲染报错忙得焦头烂额。我这边吃过一次亏之后就学乖了升级只在专门的 chore 分支做而且必须全量回归。6.3 节点布局混乱强制分组Mermaid 自动布局在节点少于 20 个时基本靠谱一旦节点多起来比如画一张完整的技术架构图要么边交叉严重要么节点堆叠。我的优化手段是尽量使用subgraph分组将同域名的服务、存储、中间件放进不同分区。subgraph 在 Mermaid 里不仅提供视觉分组还会影响布局引擎的排序。同时使用direction指令可以指定子图内部的布局方向比如外层是上下布局子图内部可以左右布局合理组合会让大图的可读性提升几个档次。6.4 大图渲染性能问题50 个节点以上的时序图或流程图渲染时间可能从几百毫秒飙升到几秒CI 里如果图很多构建时间会变得感人。我的处理策略是把渲染脚本改成增量构建只渲染 git diff 中变化的.mmd文件。这个优化的实现非常简单git diff --name-only HEAD~1拿到变更文件列表再和.mmd后缀做过滤即可。实测下来全量渲染 30 张图需要 1 分钟增量渲染一般 5 秒内结束CI 体验会好很多。6.5 多人并发改图冲突图表文件是文本理论上 Git 能自动合并但实际操作中两个人同时改一张时序图的参与者声明冲突依然会导致需要人工解决。我的经验是尽量把大图拆成小图。一张 100 行的时序图拆成 3 张 30 行的图每张职责单一冲突概率会大幅下降。还有一个容易被忽略的习惯每个文件尽量控制在一个业务用例范围内不要画“所有系统交互总图”那种图只有画的人看得懂别人改也不是看也不是最终必然腐化。7. 除了架构图diagram-design 还能用在哪些地方到这里这套方案已经能覆盖大部分技术图表的场景了。但我要说一点切身体会diagram-design 的本质是“用结构化文本描述关系”所以它的价值绝不止于画几张架构图。像依赖关系分析数据你可以把系统模块的依赖关系写成 DOT 文件用 Graphviz 自动布局十几秒就能得到一张可视化依赖图谱而且随着代码变化更新数据源就能自动重绘。又比如做数据流向梳理用 Mermaid 的 flow 图描述一张“订单数据从客户端到数据库的完整流动路线”比在 Word 里用 SmartArt 拖拽方便太多。还有一个场景是给非技术同事用。我们团队做产品宣导时把用户旅程图写成 Mermaid产品同学通过修改文本就能改变流程图分支不用麻烦技术来改图。虽然产品同学学语法也需要一点时间但比起以前“画图需要排队等设计”这个成本完全可以接受。这已经是另一个层面上的效率提升了——它让图表从“一次性交付物”变成了“让参与的每一个人都可以直接修改的活文档”。最后说一个小技巧收尾所有图表代码文件建立后记得给仓库根目录的 README 加一小段说明告诉大家diagrams/目录存在的意义以及如何修改后重新渲染。很多人不是不会用是压根不知道这套流程存在等知道了、用顺了就再也回不去手动拖拽的方式了。