做了这么多年项目,我最大的感慨就是:最容易被忽略、又最让人头疼的文档资产,永远是图。需求文档里那堆流程图、架构图、时序图,今天画完明天就过期;同事临时改了接口,图还是三个月前的版本;评审会上想临时改一下箭头方向,得在绘图工具里折腾十分钟。后来我干脆做了一个叫 diagram-design 的图表设计工作流,把写图变成写代码,把审图变成审PR。这篇博文就是这套思路的完整复盘,包括建模方式、工具选型、实操脚本,以及一路踩过的坑。
1. 图表设计这件事,到底卡在哪
1.1 手工绘图的“三座大山”
先说第一个痛点:时效性。手工拖拽画出来的架构图,本质上是一张静态图片。图片一旦生成,就脱离了代码、脱离了接口定义、脱离了最新变更。我在一个模拟项目里见过某位同学画好的部署拓扑图,刚画完第二天服务就拆了两个,图却还挂在那里,谁看谁被误导。程序员之间最怕的不是代码烂,而是文档看起来是对的,实际已经过期。
第二个痛点是协作成本。一张图几个人同时编辑,很快就会出现“你改了我不改、我改了他覆盖”的局面。图形化编辑器里没有真正的diff,没有合理的注释,也没法在改动旁边留下“为什么这么改”的上下文。评审的时候对着截图来回指,效率低得离谱。
第三个痛点,也是我之前最忽视的:复用性差。手工图里的节点、线条、颜色,都是“一次性”的。这次画了支付流程,下次画退款流程,还得从头开始拖,没有任何沉淀。时间久了,每人一套画风,同一个系统在不同的文档里长得完全不一样。
1.2 换个思路:把图当成代码来写
既然图片本身没法被版本管理、没法被自动校验、没法高效协作,那我们就换一条路:不直接画图,而是用结构化文本描述图,再由渲染引擎把它变成图。文本可以进Git,能diff,能review,能被脚本批量处理。这就是 diagram-design 的核心逻辑。
你可能觉得这多此一举,但用一段时间后会体会到它的价值。一次需求评审,我改动了一个流程分支,只需要把描述文件里的三行顺序调整一下,渲染出来的图立刻跟着变。同事在PR里看到的diff是“从节点B指向节点C的边被删除”,而不是一张完全看不出哪里变了的图片。这个体验一旦习惯,就再也回不去了。
这个思路不是要推翻所有绘图习惯。产品画给客户看的示意图、设计稿里的高保真界面,该用可视化工具还是用。但技术文档里那些描述系统结构、调用关系、部署形态的图,完全适合用文本驱动的方式来管理。简单说:凡是会跟着代码一起迭代的图,都应该离代码更近一点。
2. 拆解图表设计的共同解构:元素、层次与节奏
2.1 所有的图,都在回答一个问题:谁跟谁是什么关系
我拆解过形形色色的图:流程图、时序图、类图、部署拓扑图、状态机图。到最后发现,不管画风怎么变,底层就三样东西:节点、连线、分组。
- 节点:可以是服务、函数、步骤、状态、参与者。
- 连线:表达调用、依赖、转移、消息传递。
- 分组:把一组节点框在一起,表达子系统、泳道、环境边界。
只要把这三件事理清楚,任何图都能画。很多时候图看起来乱,不是因为内容多,而是因为作者没想清楚节点和连线的关系。比如说“用户登录”这件事,你既能画成流程图一步步走,也能画成时序图展示各系统之间怎么交互。形式不同,但它们要回答的都是同一个关系问题。
我在 diagram-design 的建模规范里要求每个人先回答两个问题:这张图里有哪些“东西”?它们之间的核心关系是哪一种?如果回答不出来,那说明设计还没想明白,不是画图的问题。很多同学一上来就开画,画到一半发现落下了某个子系统,就是因为背着关系没想清楚。
2.2 用“三层视角”控制图表的复杂度
图层一多,或者节点一多,图就会失控。我常用的办法是把一张大图拆成三个视角来看,而不是塞进一张图里:
- 全局视角:看有哪些主要子系统,做粗粒度分组,一般控制在5~8个大块。
- 流程视角:具体业务流程怎么走,聚焦在动作和转移条件上。
- 交互视角:多系统之间谁来调谁,聚焦在消息和时间顺序上。
实操中,一张图最好只有一个主导视角。比如你要画部署架构图,主线是“哪个服务部署在哪个环境中”,那就不应该把数据流转的细节都堆进来,否则节点数量直接翻倍,布局必然乱。 diagrams 想表达什么,决定了节点怎么定义、连线怎么走。这个决策要在动笔之前做,而不是边画边找。
2.3 先建模再动手,还是先画图再整理
我见过有人用白板先画草稿,再誊到工具里的,也见过直接上手写文本的。两种我都试过,最后发现更顺的是“小图直接写,大图先白板”。超过20个节点的时候,白板上理一遍逻辑,再回到文本里重写,比直接在文本里试错快很多。白板阶段不需要认真画,只需要确定三件事:核心节点是哪些、层次怎么分、哪几条关键路径不能断。
我自己的习惯是给每个复杂图表单独开一个文档段描述建模思路,写清楚“这张图用哪几个节点代表系统内组件”“为什么把这几个服务分为一组”。这相当于给图写注释,过一个月再回头看,图和文字能对上,这份文档才活得下来。
3. 选型:diagram-design 工作流里的工具组合
3.1 文本绘图工具三选一,别只盯着一个
市面上能用来写图的语言不少,我用过的比较有代表性的有三个:Mermaid、Graphviz 和 PlantUML。它们都能从文本出图,但特点差异还挺大的,不能随手选一个就用。
| 维度 | 时序图/流程图 | 复杂拓扑与自动布局 | 多角色协作图 |
|---|---|---|---|
| 语法风格 | 简洁,接近自然语言 | 声明式,专注节点和边 | 脚本化,模板感强 |
| 上手难度 | 最低,十分钟能跑通 | 中等,布局参数偏多 | 中等,需要记住关键字 |
| 自动布局能力 | 一般,适合中小图 | 很强,支持引擎级排布 | 一般,复杂图会乱 |
| 适合场景 | 需求文档、快速草图 | 架构图、拓扑图、流程图 | 用例图、类图、部署图 |
我的选择是:日常业务图和时序图用Mermaid,系统规模大、关系重的架构图用Graphviz。你不需要迷信某一个工具,重点是把工具和问题匹配起来。Mermaid写起来是真的快,但是真碰到那种一个系统几十个服务的大图,自动布局会吃力,这时候Graphviz的dot引擎反而更稳。
PlantUML我也用过,在画UML类图、状态图时非常方便,类名、方法、继承关系用文本写出来很有结构感。只不过它的生态和渲染方式在团队外部协作时稍微重了一点,我后来就只用它来画UML这一类特定图。
3.2 实际跑通一套最小工作流
先别想着搞多复杂的自动化,第一件事是让“一行文本变成一张图”的链路跑通。拿Mermaid来说,最简单的方式是装一个本地的命令行渲染器,把.mmd文件渲染成图片或网页。
npm install -g @mermaid-js/mermaid-cli mmdc -i flow.mmd -o flow.svg如果你习惯用已有渲染工具,也可以直接使用在线编辑器,但我自己更推荐命令行方式,因为能进脚本、进CI。落地这套工作流之后,目录往往长这样:
diagram-design/ ├── diagrams/ │ ├── login-flow.mmd │ ├── order-status.mmd ├── scripts/ │ └── render.sh.mmd文件就是图的源代码。改图就是改文件,渲染就是跑命令。我习惯把渲染出来的SVG和源文件一起提交,这样别人打开仓库能直接看到成品,改成代码后也能方便地对比前后变化。
3.3 批量导出:让图表成为构建产物的一部分
一套工作流真正变得好用的标志,是批量渲染。不要每次手动渲染单张图,而是写一个脚本把目录下所有图都生成一遍。这样每次改动完文本,跑一下命令,所有图表文档同步更新,彻底告别“改了旧的忘了新的”。
#!/bin/bash for f in diagrams/*.mmd; do name=$(basename "$f" .mmd) mmdc -i "$f" -o "out/${name}.svg" -b transparent -s 2 done这条脚本我在实际项目里跑了快一年,稳定可靠。还加过一个Makefile入口,把渲染挂到了make docs里。每次提PR的时候跑一下,渲染产物一起提交,reviewer就不需要本地再装任何绘图工具,直接看SVG就行。
4. 实操:从两个真实场景感受 diagram-design 的威力
4.1 场景一:用户注册流程的时序图设计
拿一个最常见的业务场景练手:用户注册。先明确角色:客户端、服务端、数据库,可能还有一个消息通知服务。关系很清晰:用户提交手机号,服务端校验,写库,发通知,返回结果。这种流程用一句话讲得清楚,但画成图能让大家在同一张图上对齐节点顺序。
我的做法是先写一个很朴素的时序图描述:
sequenceDiagram participant App as 客户端 participant API as 服务端 participant DB as 数据库 participant MQ as 消息队列 App->>API: 提交注册请求 API->>DB: 查询手机号是否已注册 DB-->>API: 返回结果 API->>API: 业务校验 API->>DB: 保存用户记录 API->>MQ: 发送注册成功事件 MQ-->>App: 异步通知渲染出来就是一张非常标准的时序图。第一版先保证内容和逻辑正确,不急着美化。跑通之后再加细节:校验失败分支、超时场景、重复点击的幂等处理。这些分支加进去之后,图会有点复杂,但这恰恰是它的价值——把隐藏在代码里的复杂逻辑提前暴露在评审会上。
4.2 场景二:微服务部署架构图设计
第二个例子更贴近系统设计:一套轻量微服务的部署架构。
digraph deploy { rankdir=TB; node [shape=box, style="rounded"]; subgraph cluster_edge { label="接入层"; style=rounded; gateway; } subgraph cluster_app { label="应用层"; style=rounded; user_svc; order_svc; payment_svc; } subgraph cluster_data { label="数据层"; style=rounded; mysql; redis; } gateway -> user_svc; gateway -> order_svc; gateway -> payment_svc; order_svc -> payment_svc; user_svc -> mysql; order_svc -> mysql; payment_svc -> mysql; user_svc -> redis; order_svc -> redis; payment_svc -> redis; }这张图的建模思路就是“分层 + 分组”。三个subgraph对应三层环境,每个层内部的节点用一致的前缀命名,跨层关系只用一条条边说清楚。布局上我只是指定了整体方向rankdir=TB,具体怎么排交给布局引擎处理,效果已经足够好了。
对比以前手工拖拽的版本,文本版本最大的优势是:改一个服务名,只需要改一行;加一个服务,只需要加一个节点和几条边。不会出现因为拖动节点导致的意外连线错位,也不会出现“我明明删了那个服务,图怎么还在”的尴尬。
4.3 一个让图表质量明显提升的小习惯
实操下来最立竿见影的习惯就是:给图和源文件同目录保存,或者像我这样在文件头写上“这张图的用途和读者”。我见过太多项目,图表文件散落各处,最后连画图的人都找不到源文件。现在我的每个图都带一个简短的说明头,标注这张图服务谁、最近一次变更原因是什么。
--- title: 支付回调处理流程 purpose: 梳理回调到账、幂等校验与重试之间的关系 updated: 配合支付超时策略优化调整 ---这段文字不参与渲染,但对后人极其友好。三个月后的你看到这行,能立刻回忆出当初为什么这么设计。这一点我觉得是文档工作最值钱的部分。
5. 踩坑实录:图表设计中的常见问题速查
5.1 渲染出来全是乱码或豆腐块
最经典的问题就是中文乱码。在英文环境下默认的渲染字体不支持中文,输出的图片全是方框。解决办法是按需指定字体,或者在你的环境中安装中文字体。
mmdc -i input.mmd -o output.svg --fontFamily "PingFang SC, Microsoft YaHei, sans-serif"不同渲染器参数略有差异,Mermaid工具链基于浏览器渲染,字体配置相对好调;Graphviz则需要配置字体的名称和备选列表。踩了一次这个坑之后,我直接在批量脚本里固化了字体参数,从此再也没被中文问题坑过。
5.2 布局乱飞,箭头交叉成蜘蛛网
文本绘图工具更适用于中小型图。一旦节点超过三四十个,自动布局会变得不可控,箭头交叉、节点重叠是常事。我的排查思路是按下面顺序来:
- 检查是不是一张图塞了两个视角,是就拆图。
- 检查是不是分组没建,该用subgraph的地方没用。
- 如果是Graphviz,试着调整
rankdir,并用不可见边控制位置。 - 实在不行,把图拆成“总览 + 细节”两层,避免硬撑。
此外,给节点命名用有意义的ID,而不是node1、node2,后续调整定位时能省大量时间。我见过同事用无意义ID画了二十个节点,想微调某个节点时完全找不到在哪,整个图基本等于作废。
5.3 图一大就卡,导出的文件动辄几十兆
大图除了难看,还影响性能。解决方法不是优化渲染参数,而是从源头控制图的规模。我定了一条规则:单张图节点不超过25个、连线不超过40条。超过了,先别急着画,回来拆图或者分层。这个数字经验来自工程实践,也和人的读图能力有关系:普通人在一张图上能处理的信息量其实很有限。
SVG是矢量格式,大文件打开很慢;PNG大图又模糊。我通常会在渲染脚本里把默认导出设置成SVG,但需要嵌入文档时,用两倍分辨率导出PNG,兼顾清晰度和体积。
5.4 多人协作时,图总是不一致
文本描述图最大的优势之一就是可以多人协作了,但团队里还是会遇到混乱,原因是没有约定谁负责某张图的所有权。后来我在团队里给每张图指定一个负责人,目录规则是“谁的业务谁画、谁的业务谁维护”。这个负责人不一定是画图最厉害的人,但一定是业务上最熟悉这张图的人。图跟着业务走,而不是跟着画图的人走,就不会出现“画完就不管了”的情况。
6. 图表设计规范的沉淀
6.1 一套够用的命名与样式约定
我后来把图的关键风格约定固化到项目文档里,新成员照着写就能保持一致。方案不复杂,就四条:
- 命名全用小写加下划线,节点ID只做标识,显示名可以随意美化。
- 分组必须加标签,凡是subgraph一定有名字,没有名字的分组宁可不分。
- 重点路径用实线主路径,次要路径用虚线或弱色,一眼能看出核心链路。
- 颜色语义化,不按心情用色:红色表示异常、失败;绿色表示成功、已确认;灰色表示辅助说明。
这些约定不需要漂亮,关键是要让团队里不同的人画出来的图风格一致。风格一致之后,跨项目看图几乎没有学习成本,这也是一种隐性生产力。
6.2 代码评审里怎么review一张图
图变成文本之后,评审流程也可以参考代码评审的范式。在评审里我不建议只看渲染出来的图,一定要连文本一起看,关注的不只是画得对不对,更要关注设计本身合不合理。每次修改图的PR,我都会要求提交说明里写清三个点:
- 这次改动是为了反映哪次业务变更。
- 为什么新增或删除了某个节点。
- 有没有和其他已有图产生矛盾。
有一次团队在评审注册流程时,发现新画的时序图和支付模块的既有图,在“超时后重试”的处理上存在冲突。如果只是看两张成品图,可能很久才能发现,但看文本里的注释和变更描述,很快就定位到了矛盾,避免了一次线上故障。这算是我把图代码化之后吃到的最甜的一颗糖。
7. 后续还可以怎么扩展
前面讲的是我自己一直在用的局部方案。围绕 diagram-design,后续可以扩展的方向其实还不少:比如把图里的节点ID映射到真实的配置项或代码类名,实现“图与实际配置的一致性检查”;再比如把渲染步骤接入接口变更的自动化流水线,接口一更新,涉及到的时序图自动进入待确认列表;又或者把图的内容与静态扫描结果做关联,让一张架构图直接展示出每个组件的健康状况。
这些方向我还没有完全做完,但底层能力已经就位了:图是结构化的文本、可以被读取和解析、可以被版本管理。只要图的数据结构是干净的,后续做分析、做自动化就都有基础。而这一切的起点,真的只是在绘图的时候,多花了几分钟去写文本描述图,而不是直接用鼠标拖。这个转换一旦完成,后续的每一份文档都会受益于这次选择。
最后分享一个小技巧:如果你只是想快速验证“某个场景适不适合文本画图”,不要看文档、不要纠结工具,直接挑一张最近经常要改动的小图,把它用文本重新画一遍,比较一下维护成本和更新效率。我自己当初就是这么切换的。试完你会发现,真正值得长期手工维护的图,其实比你想象中少得多。