做技术文档这些年,我最头疼的事不是写文档,而是画图。直到某天我在某团队的项目仓库里看到一个叫diagram-design的子目录,里面躺着的不再是一堆 PNG、JPG,而是一堆.mmd、.puml、.dot文本文件。这个小小的目录结构彻底改变了我的工作方式。今天想把这件事掰开揉碎聊一聊:怎么把“画图”这项偏手工的活儿,变成一套可控、可追溯、能进评审的工程流程。
这套思路解决的是传统画图的几个根深蒂固的痛点:图源丢失、版本不可追踪、协作基本靠微信传文件。它适合所有写技术方案、做架构设计、带团队做交付的人。不要求你会写后端,不要求你懂编程,能写 Markdown 就能在十几分钟内把图搬进版本管理。甚至哪怕你是做产品、做运营的,只要手里有需要考虑“图怎么保持长期可维护”的场景,这套方法论都吃得住。
1. 为什么非要把图当成代码来设计
1.1 传统画图方式的三宗罪
先说痛点。传统画图工具其实不差,Visio、ProcessOn、各种在线白板都挺顺手,但真正进入协作场景后问题就来了。
第一宗罪是图源丢失。这基本是所有人的共同经历:在画板里拖了一下午,把方块和箭头摆得整整齐齐,导出一张 PNG 顺手甩到群里。两周后要改图,打开桌面一看,文件夹里只剩一个图片文件,源文件不知道躺在哪台电脑的临时目录里。图片本身是没法“编辑”的,你面对的是一张已经死掉的快照。
第二宗罪是改图成本高。系统的架构图最致命的地方在于牵一发动全身。今天加了一个中间件,明天拆了一个服务,你原以为只是拉一条线的事,结果节点一多,布局整个重新排。每次改图都是一次完整的体力劳动,更别说图里还有配色、字号、对齐这些反复摩擦的细节。
第三宗罪是协作不同步。两个负责人同时改一张图,改完以后互相发文件,最后谁的版本是新的完全看命。我见过一次评审会,架构师投屏出来的时序图,跟他前一天发在文档里的根本不是同一版。他解释了半天“这张才是最新的”,底下的人已经不信了。图一旦和文字描述失去同步,文档的可信度就塌了。
1.2 代码化图表的底层优势:可渲染,也可 diff
把图写成文本,最大的好处不是“能用命令行生成图”,而是它突然拥有了文本该有的全部特质:能 diff、能评审、能复用、能自动化检查。
最直观的收益是 diff。之前改一张图,你得用肉眼去对比两张 PNG 哪里有差异,像素级别的比对简直是折磨。现在图是文本文件,两个人改了哪些节点、哪个箭头从 A 变成了 B,提交记录里一行一行写得明明白白。代码评审的时候,观察者看到的不再是“图变了”,而是“这条链路里新增了一条失败分支”,语义非常清晰。
另一个容易被忽略的能力是复用。传统画图工具里复制几个组件都费劲,而文本图源天生就是可组合的。PlantUML 支持!include引入公共组件定义,Mermaid 支持子图嵌套。团队可以把公共的服务节点、统一的模块定义抽成公共文件,谁画新图都能引用同一份定义,不会出现两个人画同一个组件却长得完全不一样的情况。
更重要的是图源和文档终于可以同仓库、同分支、同 PR 了。改代码的时候顺带改架构图,图与实现始终存在于同一个变更集里。评审人看到的是一整个变更单元,而不是“代码变了,图还是老的”。
1.3 工具选型:Mermaid vs PlantUML vs Graphviz
既然要把图文本化,选工具就成了第一道分岔口。结合我自己的使用体验,把三个主流方案放在一起做一次横评。
| 维度 | Mermaid | PlantUML | Graphviz |
|---|---|---|---|
| 语法学习成本 | 最低,Markdown 里嵌着就能用 | 中等,关键词丰富但规则性强 | 偏高,属性驱动,像写配置 |
| 渲染生态 | GitHub、GitLab、各类文档站原生支持 | 需要额外渲染服务或插件 | 独立布局引擎,需客户端或命令行 |
| 布局控制力 | 弱,靠自动排版 | 中,时序图表现力强 | 强,布局算法可调参数多 |
| 典型场景 | 流程图、时序图、甘特图、轻量架构图 | 软件工程时序图、用例图、部署图 | 复杂拓扑、依赖树、集群关系 |
| 中文支持 | 取决于渲染环境的字体配置 | 需手动设置字体,否则易乱码 | 需指定中文字体,常见于子图和节点 |
我的选型逻辑很简单:团队的技术栈决定了首选。如果大家写文档主要在 Markdown 编辑器里,那 Mermaid 几乎不需要额外部署成本,服务器端直接渲染,零依赖。如果图主要是软件工程里的时序图、类图,PlantUML 的语义表达更贴近开发者的思维习惯,参与者生命线的处理在同类型工具里几乎是最好的。而一旦涉及复杂的依赖关系、集群拓扑,Graphviz 的 dot 语法反而是唯一能撑住场面的,它能自动计算出大多数工具都算不出来的优雅布局。
我个人现在的组合是:日常文档里的流程、链路图一律用 Mermaid;复杂的软件调用时序和部署架构用 PlantUML;真正需要描述节点关系的拓扑图交给 Graphviz。这套组合帮我覆盖了 95% 的作图场景。剩下的 5%,比如需要像素级自由摆放的场景,说实话,文本工具确实不适合,那就老实回到画板里去画。
2. 图表设计的核心细节与实操要点
2.1 一张图只讲一个主题
刚才说完工具,接着聊比工具更要命的:图的组织方式。文本化图源最大的敌人不是工具能力不足,而是人类把好几件事塞进一张图里。
我见过最典型的案例是有人画了一张“系统全览图”,里面既有服务间的调用关系,又有部署的物理位置,还带几条业务流程的时序线。一张 A4 纸满满当当,打印出来根本看不清。问题不在画图技巧,而在叙事目标不清晰。
一张好的图,读者瞄一眼就应该能回答一个问题:这张图在讲什么?如果答案是“既讲部署又讲调用还讲时序”,那这张图的设计就失败了。正确做法是拆成多张图,形成一棵树:一张系统全景图描述边界和外部依赖;下面若干张子图分别描述服务间的调用关系、关键业务时序、部署环境拓扑。每一张图承担一个叙事职责,读者按需钻取。
拆图的另一个好处是后续的维护成本骤降。全景图里的某个服务变了,你只需要改一处;如果所有信息都堆在一张超图里,任何小变化都会引发连锁修改,改一次图熬一次夜。
2.2 布局方向与语义分组:信息流动要符合阅读习惯
文本图源虽然在排版上控制力弱,但有两个参数是设计阶段就必须定下来的:方向与分组。
方向的选择本质上是在模拟阅读顺序。graph TD是自上而下,适合表达层级、组织结构、命令链路;graph LR是自左向右,适合表达时间流动、请求链路、流水线。经验法则很简单:如果图里存在“谁在谁之上”的层级关系,比如一个系统包含若干模块,用 TD;如果存在“先发生什么后发生什么”的先后顺序,比如一次请求经过网关进入服务,用 LR。方向一旦选反,图读起来会非常别扭,连渲染之后的布局都会显得别扭。
分组则是用subgraph或者 PlantUML 里的package进行语义聚类。节点不是散落的点,而应该天然带有归属感。图上所有数据库节点归到“数据层”,所有外部依赖归到“第三方系统”,这样读者不需要逐行看图就知道某一个区域表达的是什么。字符串的命名也要统一,节点里的文案用名词短语,连线的箭头处如果需要标注,用“动词+结果”的短语,比如“返回成功”“扣减库存”。这些细节不花什么成本,但对图的可读性提升非常明显。
2.3 颜色不是装饰,是语义
文本图源虽然写的是字,但渲染出来仍然是彩色的,这里藏着一个大学生写代码最常见也最容易忽视的坑:配色全凭个人好感。
在一个团队里,图源文件是共享资产。你今天用蓝色表示外部依赖,明天另一个同事用绿色表示外部依赖,下游读者看两张图时脑子里就得多做一次映射转换。正确的做法是先定一套语义色规范,然后所有人严格执行。
我的习惯是:稳定组件用中性色,比如灰色或深蓝;本次新增的逻辑用高亮色,比如绿色;标记废弃的内容用红色加删除线;外部依赖统一用橙色或黄色的浅色块。这样一张图上,读者第一眼就能区分哪些是重点、哪些是背景、哪些是风险点。除了颜色本身,注意不要只靠颜色表达状态,至少辅助一种形状或文字差异,确保色弱读者和黑白打印场景下图依然可读。
3. 实操过程与核心环节实现
3.1 案例一:用 Mermaid 从零画一张系统链路架构图
用实际案例来说话。假设我在做一个模拟项目 X,一套最精简的电商下单链路:浏览器到网关,网关分发到订单、库存、支付三个服务,底层挂 MySQL 和 Redis。用 Mermaid 实现看图:
%% diagram-design 演示:下单链路架构 graph LR subgraph 客户端 A[浏览器] end subgraph 接入层 B[API 网关] end subgraph 核心服务 C[订单服务] D[库存服务] E[支付服务] end subgraph 数据层 F[(MySQL)] G[(Redis)] end A --> B B --> C C --> D C --> E D --> F D --> G E --> F第一行graph LR确定方向为左到右,因为这是一条请求链路,有明确的流向;如果改成graph TD,四个服务竖着排下来会非常长,阅读时很容易断流。用subgraph把四个区域框出来,渲染效果等于四个带颜色的容器,语义分界一目了然。数据库节点写成F[(MySQL)],Mermaid 使用双括号语法渲染成圆柱体,一眼就知道这是存储。
语法层面展开看:节点定义A[浏览器]里的A只是坐标代号,真正显示在图上的是方括号里的文字;%%是注释,建议每个图源文件头部都写一行注释记录图的用途,甚至标注最后一次有效渲染的版本号。这个习惯一开始会觉得多余,线一旦复杂起来,注释就是救命稻草。
3.2 案例二:用 PlantUML 把一次下单流程的时序画清楚
架构图画的是静态关系,时序图画的是动态交互。这个场景我用 PlantUML,因为它在表达“谁在什么时间段内调用了谁”这件事上明显更强。
@startuml actor 用户 participant "Web" as web participant "订单服务" as order participant "库存服务" as stock participant "支付服务" as pay 用户 -> web: 提交订单 activate web web -> order: 创建订单 activate order order -> stock: 扣减库存 activate stock stock --> order: 扣减成功 deactivate stock order -> pay: 发起支付 activate pay pay --> order: 支付成功 deactivate pay order --> web: 下单成功 deactivate order web --> 用户: 展示订单结果 deactivate web @endumlactor定义了一个小人图标,代表系统外的真实用户;participant "Web" as web里的别名web,是为了后面写箭头时不至于每次都敲一长串文本。activate和deactivate是一对指令,控制生命线的激活长度,渲染出来后每个服务头顶会多出一段垂直的长条,表达它正处于调用期的占用状态。箭头方向也分两类:->是同步调用,实线;-->是返回结果,虚线。这一套约定映射了程序调用和返回的真实关系,看图的人不需要你额外解释。
这个例子是典型的成功路径,实际业务通常还有分支。PlantUML 里用alt 描述 / else 描述包一层,就能把成功和异常两条路径在同一个时间维度上展开对比。时序图最忌讳把多条分支写成并行的独立片段,用 alt 结构明确表达取舍关系,后续做流程复盘会轻松很多。
3.3 把图嵌入文档流水线:从画图到维护的一体化
上面两个案例解决的是“怎么画”,接下来是“画完之后怎么办”。
图源文件只有在文档体系里落地才有价值。我的做法是在自己的技术博客和项目文档里直接使用 Mermaid 的 Markdown 区块,渲染环境原生支持,不需要单独维护图片文件。对于必须导出独立图片的图,比如对外方案书要嵌入 Word,建议统一导出 SVG 而不是 PNG,SVG 是矢量格式,缩放不模糊,而且文本可复制。命令行指令大致是这样:Mermaid 用mmdc -i input.mmd -o output.svg,PlantUML 用plantuml input.puml -tsvg。
CI 层面还有一个值得做的操作:语法校验。因为 Mermaid 和 PlantUML 的渲染过程不会永远正确,写的语法一旦有问题,文档站会静默失败,只留下空白块。把校验集成到流水线里,文本图源变更时自动执行一次渲染,语法错误直接让流水线挂掉,从源头上堵住图源坏掉的问题。这些校验脚本本身也只有几十行,但价值非常大。
3.4 几个排版参数的实战心得
最后补几个我在实际打磨中积累的排版细节。
Mermaid 全局配置可以写在文件首行的注释里,比如:
%%{init: {"flowchart": {"nodeSpacing": 60, "rankSpacing": 40}}}%%nodeSpacing控制同一层级内节点的水平间距,rankSpacing控制层级之间的垂直间距。默认值在节点少时还行,节点一旦超过六个,间距太紧会让连线贴住文字,太松又会让整张图撑得太宽。调的时候以渲染后 20 英寸显示器下不缩放可读为准。
PlantUML 的中文乱码问题几乎每个人都会碰上,根因是渲染环境找不到匹配的中文字体。在文件里用skinparam defaultFontName "Microsoft YaHei"声明字体名称,绝大部分情况下就能解决;如果渲染环境在服务端,还要确认这个字体真的装到了系统里。这个坑要是没提前处理,画出来的中文全是方块,等于白画。
4. 常见问题与排查技巧实录
4.1 布局乱飞、连线交叉
文本画图工具最让人沮丧的情况就是“语法全对,但渲染出来的线绕地球一圈”。核心原因通常是层级太深或分支太多。比如一张流程图中,有一个节点引出了七八条并行分支,自动布局算法只能把这些分支像扇形一样铺开,必然会交叉。
遇到这种问题,我的排查顺序是先数一下节点:如果单层节点数量超过 12 个,先考虑拆图而不是调参数。其次是通过subgraph强制把相关节点归到同一组,布局算法对分组内部的重排优先级会明显提高。最后再做微调,给nodeSpacing和rankSpacing加数值,而不是反复修改节点之间的连线方向。
4.2 中文乱码
中文乱码分为两种。一种是渲染结果里中文全是方框,这种情况基本就是环境字体问题,按前面说的方式设置字体;另一种更隐蔽——图里某些中文正常、个别字符变成问号,通常是因为源文件的编码不对,UTF-8 无 BOM 是当前所有主流工具的默认要求,建议把编辑器的编码设置为 UTF-8,并且关闭自动添加 BOM 的选项。
排查的时候我一般会把一张复杂图简化成只含两三个节点的最小样本,先验证环境通不通,再逐步叠加内容,定位效率高很多。不要拿着一张大图一点点找问题,那是浪费时间。
4.3 团队协作中的“图源漂移”
所谓图源漂移,是指导出的图片文件和源文件脱离了对应关系。最常见的情况是:有人把导出的 SVG 提交到了文档目录,源.mmd却没提交;或者相反,源文件在仓库里更新了,但文档站引用的图片路径还是老的。
我的强制约定是:图源文件和它导出的图片必须放同一个目录,文档里引用相对路径;图片文件的命名和源文件完全一致,只是后缀不同。任何没有源文件的图片,一律视为临时产物,不允许进入正式文档。这个约定虽然看起来死板,但它能让图源、导出图、文档引用三者的生命周期完全绑定。
4.4 版本冲突和合并地狱
文本图和代码一样,也面临多人同时修改的合并问题。当一张图的节点特别多、两个人改动的区域又恰好相邻,Git 冲突几乎是必然的。处理这个问题的第一原则是把图拆小,文件小到冲突的概率就低;第二原则是每个图一个独立文件,千万不要把多张图塞进同一个文件里管理;第三原则是提交信息里必须写清楚改了什么语义,比如“增加库存不足分支”,而不是“更新图”,这样冲突的时候才能快速判断该保留哪个版本。
我的一个小习惯是每次修改图源后,顺手在文件头部的注释里更新版本号,哪怕只加一。等到几个人各自改了文件、合并时互相不知道谁新谁旧,这个注释就是最直接的判断依据。虽然是笨办法,但已经帮我多次避免了讨论到一半还要去翻提交历史的窘境。
这个内容后续还可以继续扩展。比如配合脚本定时生成图片、把图源纳入自动化的架构治理平台、甚至用代码生成器的思路,让图源直接从配置或代码注释里生成。但核心始终是那一条:先把图当成代码来对待,后面所有的自动化和规范化才有立足的地方。