画图这件事,做了十年软件我都绕不开它。方案评审要画架构图,代码注释里要放时序图,README 里得补一张流程图,连给运营同学讲数据链路时都得靠 diagram。diagram-design 说白了,就是“把脑子里那幅图,变成别人能看懂的一幅图”。这句话很朴素,但能做到的人真的不多。我见过太多团队,图是画了,画完自己都改不动,协作时更是灾难——有人用 Visio,有人用 draw.io,有人拿 PPT 拼方块,最后文件全变成“最终版_v3_修改2”躺在网盘里。
这一篇我会用实际踩坑换来的经验,把从设计思路、工具选型,到落地实现、常见坑位的完整路径都走一遍,重点说说为什么我最终选择了代码化制图这个方向,以及怎么让你的图能持续维护、能跟着代码演进。内容适合正在给项目画图、想用代码管理图表、或者刚接手一个没有文档的技术团队的人。不需要你有图形设计基础,能写 Markdown 基本就能跟上。
1. 先想清楚:你画的到底是什么图
1.1 图表不是装饰,它是信息压缩的效率工具
先给图找一个正确的定位。有人把图当交付物,觉得画得越满越有工作量,于是架构图里塞 80 个方块,评审时没有一个人愿意认真看。有人把图当思维整理,想到哪画到哪,最后只有画图的人自己能看懂。还有少部分人把图当沟通媒介,一切设计都围绕“对方能否在 20 秒内看懂核心结构”展开。我建议你采用第三种心态。
为什么是 20 秒?因为现代人的注意力和耐心都非常有限。评审会上,大家扫一眼图,能建立初步心智模型,才有可能对细节提出有效问题;如果这张图第一眼就是密密麻麻的节点和箭头,绝大多数人只会礼貌性点头,然后继续低头看手机。好的 diagram-design,本质上是在做信息压缩:把一段需要两千字才能说清的结构关系,压缩成一张 20 秒能读完的图。但压缩一定有损失,有损失就必须做取舍,只保留目标读者当前最需要的信息层次。
所以,动手前第一件事不是打开工具,而是判断这张图存在的理由是什么。是帮助新人理解主链路?是评审时讨论模块边界?还是上线前确认部署关系?理由不同,图的内容和抽象粒度完全不同。理由不清晰,图画得再漂亮也只是自我感动。
1.2 常见图型与适用场景速查
在动手之前,先知道自己要画的是哪一类图。不同图型解决的问题不一样,选错图型,表达效果直接打折一半。
- 流程图(flowchart):主要表达“先做什么,再做什么,判断后走哪条路”。典型场景是业务流程、接口处理流程、部署发布流程。判断标准很简单:如果文字描述里反复出现“如果、否则、然后”,大概率就是流程图。
- 时序图(sequence diagram):表达“谁在什么时候调用谁,按什么顺序传什么参数”。典型场景是多系统交互、分布式链路追踪、SDK 调用过程。如果核心是多个参与者之间的先后顺序,而不是流程判断,就用时序图。
- 状态图(state diagram):表达“一个对象在生命周期里有哪些状态,事件触发后怎么转移”。典型场景是订单状态、微服务实例状态、审批状态。
- 类图与 ER 图:表达“数据结构与关系”。类图偏代码层,ER 图偏数据库层,两者侧重点不同,但很容易混淆。
- 架构图(architecture diagram):表达“系统由哪些模块组成、模块之间如何依赖、流量如何流转”。它其实是多种图型的组合,也最考验设计师的抽象能力。
- 数据图(饼图、柱状图、折线图):表达量化信息,一般用于汇报和指标分析。
| 图型 | 核心问题 | 典型场景 | 选错时的表现 |
|---|---|---|---|
| 流程图 | 接下来按什么顺序执行 | 业务流程、CI 流水线 | 画成架构图后看不出先后顺序 |
| 时序图 | 参与者之间怎么交互 | 接口调用、事务过程 | 画成流程图后看不出调用方 |
| 状态图 | 对象如何迁移 | 订单状态、实例生命周期 | 大量箭头交叉,可读性差 |
| 类图/ER 图 | 类和表之间如何关联 | 领域模型、库表设计 | 信息过载,评审无人看 |
| 架构图 | 模块与依赖边界 | 系统设计、部署方案 | 忽略流量方向,看不出主链路 |
| 数据图 | 数值关系变化 | 报表、指标分析 | 用流程图硬套,信息失真 |
选型时还有一个常见误区:以为一张图能承担所有职责。实际上,越是复杂的系统,越需要多种图型配合使用,例如用一张架构图展示全貌,再用若干张时序图补充关键链路的交互细节,各司其职,比硬把所有信息塞进一张图里要清晰得多。
1.3 设计之前先回答 5 个问题
我每次画图前都会在脑子里过一遍这 5 个问题,简单但非常有效。
给谁看?这决定了术语密度和信息分层。给技术团队看,可以放心画 JVM、数据库连接池、消息队列;给业务方看,只画“入口、能力、出口”就够了,连节点命名都要换成业务语言。
想表达什么?一张图只讲一个核心观点。常见陷阱是“想在一张图里既展示全貌又展示细节”,结果两个都没讲清。如果发现自己正在补充很多旁支信息,说明该拆图了。
信息层级有多少?如果一张图里超过 30 个节点,我会强烈建议拆图。节点越多,布局越难、连线越乱、改图越累,读者接收信息的效率反而越低。
布局方向是什么?流程类一般从上到下,时序类从左到右。方向在代码化工具里只是参数,但在设计时要提前统一,混用方向会让人迷失。
这张图要活多久?如果是临时涂鸦,画多丑都行;如果是长期文档,就必须考虑可维护性。这个问题直接影响下面的工具选型。
2. 工具选型:为什么我最终选了代码化制图
2.1 主流方案横向对比与适用边界
市面上的画图工具五花八门,按照“是否代码化”可以粗分成两大类:拖拽类和代码类。拖拽类代表有 draw.io、ProcessOn、Excalidraw、Visio;代码类代表有 Mermaid、Graphviz/DOT、PlantUML。两类我都深度用过,下面这张表是我个人的直观感受。
| 方案 | 学习成本 | 版本管理 | 自动布局 | 维护成本 | 适合场景 |
|---|---|---|---|---|---|
| 拖拽类(draw.io 等) | 低 | 差 | 手动 | 高 | 一次性草图、白板讨论 |
| Mermaid | 低 | 极好 | 自动 | 低 | 文档、README、团队规范图 |
| Graphviz/DOT | 中 | 好 | 强 | 中 | 复杂关系图、自动生成图 |
| PlantUML | 中 | 好 | 自动 | 中 | UML 语义强、代码建模 |
拖拽类工具的共同问题是:图的背后没有一个清晰的数据模型,你保存的只是一个视图工程文件。一旦结构发生变化,要增加节点或者改连线,就必须手动拖拽、重新对齐、调整跨线,改一次至少 20 分钟。代码化制图则不一样,你改的是声明式文本,布局算法会自动重排。对高频变化的项目文档来说,这个维护成本差异非常明显。
Graphviz 的强项在于节点关系非常复杂时,它的自动排版算法仍然能给出相对稳定的结果;PlantUML 则更贴近软件工程语义,类图、时序图、用例图都有专门语法。两者都有学习曲线。Mermaid 虽然功能不算最全,复杂的美化效果也做不了,但它是“文本 + 图”工作流里最顺滑的,语法简单到像写 Markdown 一样,这也是我日常选用它的原因。
2.2 代码化给团队协作带来的三个隐性收益
选代码化制图,不只是自己改图方便,更重要的是它改变了整个团队的协作方式。我有三个很深的体会。
第一个是 diff 可读。拖拽图保存后通常是二进制或私有格式,代码评审时根本没法逐行 review,别人只能看到“哦,图变了”,但说不清哪里变了。Mermaid 这类纯文本图可以直接出现在 PR 的 diff 里,改没改、改了什么,一眼就能看出来。这一点对技术团队非常重要,因为图常常是评审的重点对象。
第二个是可复用、可生成。既然图是文本,就可以在模板、脚本里动态生成。我做过从数据库元信息自动生成 ER 图,也做过从接口清单自动生成依赖图,这在拖拽类工具里几乎不可能实现。数据变化时,图跟着重新生成,永远比手工维护的图准确。
第三个是和文档系统无缝集成。GitHub、GitLab 对 Mermaid 有原生渲染支持,Markdown 文件里直接写代码块就能出图;mkdocs、VitePress 也有相应插件。这意味着图不需要单独维护一个文件,它可以作为文档的一部分提交和版本化,图的“居住地”和代码在一起,天然不会失联。
3. 核心语法拆解:以 Mermaid 为例画结构图与流程图
3.1 最小可运行示例与基本方向
Mermaid 是代码化制图方向上一个很有代表性的工具,也是我日常用得最多的一个。虽然它不是功能最全的图工具,很多复杂效果做不到,但它在“文本 + 图”的工作流里体验最顺滑。先看一个最基础的 flowchart。
flowchart TD A[解析请求] --> B{校验权限} B -- 通过 --> C[执行业务逻辑] B -- 拒绝 --> D[返回错误]第一行flowchart TD是图声明,TD 表示 Top to Down,从上到下。要改成左右布局,就写flowchart LR,也就是 Left to Right。后面的每一行都是一个节点或一条连线:A、B、C、D 是节点 ID,方括号表示普通矩形,花括号表示菱形判断,-->是带箭头的实线。这就是 Mermaid 的核心模型:声明节点,再声明节点之间的边。剩下所有的语法,都是在这两个动作上做扩展。
方向的选择也有一点讲究。从上到下适合流程步骤相对线性、判断分支不复杂的图;从左到右更适合表达“调用关系”,因为调用顺序天然是从左到右逐层深入。我的习惯是:业务流程图用 TD,系统调用图用 LR,这样第一眼的信息方向就符合直觉。
3.2 节点写法与常用形状
节点是图的基本单元。Mermaid 提供了多种形状,每种形状在语义上都有对应角色,我常用的几种如下。
A[文本]:矩形,普通节点,最常用。A(文本):圆角矩形,适合开始或结束节点。A((文本)):圆形,适合子流程入口或出口。A{文本}:菱形,适合判断分支。A>文本]:非对称形状,适合输出或外部实体。A[[文本]]:子程序或模块的表示。A[/文本/]:平行四边形,适合输入输出节点。
这些形状不需要硬记,做图时按角色选用即可。我的习惯是:菱形只用来表达判断,矩形只用来表达数据处理,圆角矩形表达起止点,这样读者只需要看一眼形状就能猜到节点性质。节点 ID 建议只用英文和数字,显示文本放在语法符号的括号里就好,中文标签可以直接放进去,但千万别在 ID 里放中文或空格,否则解析很容易出问题。
3.3 连线语义:箭头、标签、虚线、子图
图的信息量其实大部分来自连线,而不是节点本身。连线的类型直接影响读者对依赖关系的判断。
-->:实线箭头,表达流向或依赖方向,最常用。---:无箭头实线,表达关联或相邻关系。-.->:虚线箭头,表达异步、间接或弱依赖。==>:加粗箭头,表达强流程或重点链路。-- 文案 -->或|文案|:给连线加标签,说明关系内容。
A -- 创建订单 --> B A -. 异步通知 .-> C A ==> 核心链路 ==> D需要给模块分组时,用 subgraph 语法。凡是属于同一个模块的节点,放进同一个 subgraph 里,图的层次会清晰非常多。
subgraph 订单模块 O1[创建订单] O2[订单查询] end需要注意的是,subgraph 的 id 不能带空格,标签要写在 id 后面,某些旧版本写法差异会导致渲染失败,团队里最好固定一种写法。另外,连线标签不要写得像代码注释一样长,控制在六到八个字以内,否则线上文字比节点还显眼,干扰视线。
3.4 样式与主题定制
样式层面的定制主要有两个入口:classDef 和主题变量。classDef 是为某一类节点定义统一样式,比如给“核心服务”和“外部依赖”用不同填充色。
classDef normal fill:#e8f4f8,stroke:#333,stroke-width:1px class A,B,C normal主题变量则在图开头用%%{init: ...}%%设置,可以调整整体配色、字体、连线曲率等。
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#fafafa", "fontSize": "14px"}}}%%我的建议是:颜色是用来传达语义的,不是用来好看的。定一套固定色板就够用了,比如核心服务用蓝色系、外部依赖用灰色系、异常链路用红色系,全图颜色种类不超过四种。颜色一旦太多,图就变成了彩虹,读者反而抓不住重点。团队规范图尤其要统一配色,防止每人画一个版本。
4. 实操案例:给“在线订单系统”画一张可持续维护的架构图
4.1 场景设定与最终效果目标
假设你刚接手一个订单系统,新同事问主链路时,总不能从数据库表讲起。此时需要的是一张能让不熟悉系统的人在 1 分钟内看懂“用户请求是怎么进来、经过哪些模块、最终落在哪些存储上”的架构图。这个目标决定了抽象粒度:只画主链路上的模块,不画内部实现细节,更不画每一行的具体处理逻辑。
这个例子我选在线订单系统,是因为它是典型的后端业务系统,几乎所有读者都能理解它的业务背景。最终效果应该包含:客户端入口、网关、核心服务、异步消息、数据库与缓存,以及日志告警这些横切能力。不要一上来就追求完整,先画出主干,再逐步补充依赖。
4.2 分步实现:从文本草稿到完整图
第一步,把节点先列出来。这一步不排版、不连线,只把概念打散到桌面上。比如:Web 端、Nginx 网关、订单服务、库存服务、支付服务、消息队列、MySQL、Redis、日志系统、告警系统。这个环节如果发现概念粒度不统一,比如既有“订单服务”又有“订单超时状态机”,说明信息层级混了,要先想清楚再继续。
flowchart TD Web[Web端] Gateway[Nginx网关] Order[订单服务] Stock[库存服务] Pay[支付服务] MQ[消息队列] DB[(MySQL)] Cache[(Redis)] Log[日志系统] Alert[告警系统]第二步,把主链路连出来。先画最关键的调用顺序:Web 端请求到网关,网关到订单服务;创建订单需要同步调用库存服务和支付服务,再发一条消息到 MQ。这一段不需要追求完整,先把主干理清楚,让它能表达“一条订单请求是怎么走完主流程的”。
Web --> Gateway Gateway --> Order Order --> Stock Order --> Pay Order --> MQ第三步,加依赖存储与旁路。订单服务读写 MySQL 和 Redis,库存和支付也有自己的存储;MQ 的消费方属于旁路,比如订单超时关单,它不是用户请求的直接路径,但确实依赖消息队列。日志和告警是横切能力,用虚线连接到相关服务即可。到这一步,我开始意识到“Order 到 MQ”只画一条线很难表达“发送事件”和“消费事件”两个角色,于是拆解成两个节点:MQ 生产者侧和消费者侧。
第四步,用子图分组并调整视觉层次。把网关集群、订单域、基础组件各放进一个 subgraph,再给核心服务加色,外部依赖用灰色。完整代码基本长这样:
flowchart TD subgraph Client[客户端] Web[Web端] end subgraph GatewayLayer[接入层] Gateway[Nginx网关] end subgraph OrderDomain[订单域] Order[订单服务] Consumer[关单消费者] end subgraph Deps[依赖服务] Stock[库存服务] Pay[支付服务] end subgraph Middleware[中间件] MQ[(消息队列)] DB[(MySQL)] Cache[(Redis)] end Web --> Gateway Gateway --> Order Order --> Stock Order --> Pay Order --> MQ MQ --> Consumer Order --> DB Order --> Cache classDef core fill:#e8f4f8,stroke:#1a73e8,stroke-width:2px classDef dep fill:#f5f5f5,stroke:#999,stroke-width:1px class Order,Gateway core class Stock,Pay,Consumer dep到这一步,图已经能说明问题了。如果还想再严谨一点,可以把“订单超时关单”这个异步链路的标签写清楚,比如Order -- 发布超时事件 --> MQ,再把MQ -- 投递事件 --> Consumer补上,读者就不会产生“这消息是发给谁”的疑问。
4.3 把图接进文档工作流:README、CI 与团队协作
图画完只是开始,真正难的是让它一直保持更新。我见过不少项目,架构图在评审当天很漂亮,半年后和代码已经没有关系了。要解决这个问题,必须把图嵌进文档和开发流程。
首选做法是放在仓库根目录的docs目录下,命名成architecture.md,在 README 里加一个链接。由于 GitHub 和 GitLab 都能直接渲染 Mermaid 代码块,图不需要导出成图片文件,直接以源码形式存在仓库里。每次代码结构变化,顺手更新这个文档;如果依赖关系改了但图没改,代码评审时其实很容易发现,因为 diff 里能看到图文件没有变化。
如果需要导出图片给非技术同事,或者放进对外方案书,可以用 mermaid-cli 的命令行工具,例如:
npx @mermaid-js/mermaid-cli -i input.mmd -o output.png这条命令会启动一个无头浏览器完成渲染,导出的效果和本地预览一致。还可以把它写进 CI,在文档变更时自动生成 PNG 并作为构建产物。团队协作时再补两条约定:一张图只有一个 owner,改动由 owner 负责;提交 PR 时如果涉及模块依赖,必须在同一个 PR 里更新图。这些约定不需要额外工具,写在 CONTRIBUTING 文档里就够了。
5. 常见问题与排障技巧实录
5.1 渲染与语法问题速查表
我整理了一张速查表,都是实际使用中遇到频率最高的问题。遇到图渲染不出来,先对照这张表排查一遍。
| 症状 | 原因 | 解决办法 |
|---|---|---|
| 节点内容变成纯文本,没有渲染成图 | Markdown 代码块语言标识写错 | 在文档里把代码块语言标识为 mermaid,本地 CLI 用 mmdc 处理 |
| 图渲染时报语法错误 | 节点 ID 里放了空格或中文 | ID 只用英文、数字、下划线,显示文本放在括号里 |
| 中文标签乱码 | 旧版本 Mermaid 或导出环境缺中文字体 | 升级工具版本;导出时安装中文字体并指定 font-family |
| 箭头方向画反 | 混淆了 A --> B 的方向语义 | 记住箭头指向的就是流向目标,从源到目标书写 |
| subgraph 没有显示成矩形分组 | subgraph id 含空格,或 id 大小写不匹配 | subgraph 的 id 不含空格,后续引用大小写保持一致 |
| classDef 风格不生效 | class 语句写在了 classDef 定义之前 | 先定义 classDef,再写 class 赋值 |
| 主题变量初始化报错 | init 里的 JSON 格式不对 | JSON 用双引号,不要加注释,写完检查括号 |
| 图很大时渲染卡顿 | 单图节点过多 | 拆成总览图加局部详图,一张图不超过三十个节点 |
5.2 我踩过的三个典型坑
第一个坑:本地渲染正常,推到 GitHub 上却报错。原因是不同平台的 Mermaid 版本不一致,旧版本对某些语法和新特性支持不好,尤其像&、<、>这类特殊字符,处理方式差异很大。后来我强制要求团队统一版本,文案里避免特殊字符,需要展示时用 HTML 实体写法,这个问题就很少再出现了。
第二个坑:用 mermaid-cli 导出 PNG 时中文全部变成方块。排查下来不是代码问题,是容器里没有中文字体。解决方法是给导出环境安装中文字体,比如 Noto Sans CJK SC,然后在 CSS 里指定font-family: "Noto Sans CJK SC"。这里要提醒一下,本地编辑器预览正常不代表 CLI 导出正常,两条链路要分开看。
第三个坑:一张图塞了 100 多个节点,布局算法跑出来交叉线多到没法看。我把锅甩给工具,后来才明白是设计问题。节点太多时,任何自动布局都救不了可读性。正确做法是拆图:总览图只画模块和主链路,局部详图单独展开,节点之间用 click 跳转关联起来,读者需要看细节时再进去。
5.3 团队落地阶段的三条建议
最后说三条团队落地建议,都是我实践过觉得比较有性价比的约定。
第一,单一图单一职责。一张图只表达一个核心观点,节点数量控制在三十个以内。如果超过三十个节点,说明这张图承担了两个以上职责,拆开反而更清晰。
第二,图随代码走。图文件放进代码仓库的 docs 目录,和 README、接口文档一起版本化。代码改了,图也必须跟着改,把“更新图”养成肌肉记忆。
第三,固定语义色板。全团队统一颜色规则,核心服务用什么色、外部依赖用什么色、异常链路用什么色。视觉一致性看似小事,长期维护时能省掉大量沟通成本。
我在实际项目里有个朴素习惯:每次画完图,会找一个不懂这个系统的人来看,只看 20 秒,然后让他描述自己看到了什么。如果他说的和我最初想表达的差不多,说明这张图合格;如果他张口就问“这是什么、那是什么”,说明图里的职责边界和信息层级还没理清楚。这个测试成本很低,但比任何工具技巧都管用。diagram-design 说到底不是艺术比赛,它是让团队少开几次无效评审、少写几段重复解释的技术动作。希望这篇文章能让你下次画图时,少走我走过的弯路。