前两天同事抱着一块白板来找我,说他负责的模块里那张系统交互图怎么都画不清楚:节点摆来摆去,箭头改了几轮,最后连他自己都看不懂了。我看了他一眼,在终端里敲了十几行文本,渲染出来一张结构清晰的图,他愣了足足三秒钟。这个画面我一直记着,因为它引出了我今天想聊的主题:diagram-design——图表设计。
别误会,diagram-design 不是说用哪个画图工具好、哪个图标库好看,它是把“画一张图”这件事当成一个有章法的工程流程来对待:先想清楚给谁看,再定图类型,然后梳理信息结构,最后才落到布局、颜色和渲染。这些年我在技术方案评审、系统文档、项目复盘里画过大量图,踩过不少坑,也沉淀下来一套相对稳定的做法。这篇文章就是把整套思路、工具选型、实操步骤和避坑记录整理出来,给那些经常要画架构图、流程图、时序图的工程师和文档写作者一份可以照着做的参考。
1. 为什么我最终选择了“图表即代码”这条路线
1.1 从白板困境到版本化需求
白板是讨论问题的最快载体,但也是信息流失最严重的载体。我有过太多次这种经历:评审会上用白板画了张调用关系图,大家讨论得很热烈,散会时拍了张照片放进文档,三个星期后再看,照片里某个方框写的是什么已经完全分辨不出来了,更别提有人想在上面改一条线。
这个问题背后的本质是:图表和代码一样,是一种需要持续变更的信息载体。只要系统在演进,图就必须跟着变。而传统拖拽式绘图工具默认把图表当成“一次性产出物”——画完就导出图片,导出之后和源头就断了。图片进入文档就变成了死信息,无法搜索、无法对比、无法合并,改一版就要重新导一张,时间一长没人记得哪张图对应哪个版本。
我后来把图表管理的思路彻底调整成和代码管理同构:图表由文本代码生成,图的源文件进入版本仓库,每次变更都提交可追踪的 diff。这样图不再是一张静态图片,而是像一段可维护的代码资产。
1.2 文本绘图相比拖拽绘图的核心优势
我并不是说拖拽工具完全没用,但在大多数工程技术场景下,文本绘图有四个拖拽工具很难替代的优势。
版本可追踪是最直观的一点。图源文件是纯文本,Git 天然支持逐行对比。昨天改了一个节点名称还是改了一条连线的方向,通过 diff 一眼就能看出来。拖拽工具的快照式图片做不到这种粒度。
复用与抽离意味着你可以把公共节点定义、样式片段抽出来复用。比如一个系统架构图里有三个服务节点需要保持视觉一致,文本绘图可以用变量或函数统一生成,改一处全局更新;拖拽绘图里三个节点是三个独立对象,改样式得逐个改。
自动化链路打通意味着图和文档、图与发布流程可以无缝衔接。文档里嵌入一段图表代码,构建时自动渲染成图片,永远不需要手动上传图片再担心它过时。这个流程拉通之后,文档维护成本会大幅下降。
评审方式也更接近代码评审。图表改动随 PR 提交,评审人通过 diff 就能看出关系变化,可以直接在评论里讨论某一条边该不该连,体验比对着两张 PNG 图片找不同好得多。
1.3 这套路线适合谁、不适合谁
聊了这么多好处,也得说清楚边界。我的经验是,这路线适合在以下场景中的人:系统规模在持续演进的业务、有多人协作维护文档需求的团队、需要把图表嵌进自动化文档流程的工程师。
但它不是万能的。纯创意阶段的头脑风暴,信息结构特别发散的时候,用代码画图就像戴着镣铐跳舞,不如一张白板加三支彩色马克笔效率高。另外,如果团队里大多数人并没有代码习惯,维护文本图源会变成额外负担,这种情况下倒是可以保留拖拽绘图,但要人为约束“图必须随版本更新”的机制。工具永远服务于协作方式。
2. 我的工具链组合:三类绘图语言的区别使用
2.1 工具对比:从语法成本到渲染效果
我现在的工作流里常驻几套图表语言,它们不是替代关系,而是各自负责不同类型的图。下面这个表概括了核心差异。
| 工具 | 语法成本 | 擅长场景 | 渲染质量 | 交互能力 | 生态集成 |
|---|---|---|---|---|---|
| Mermaid | 低,5分钟上手 | 流程图、时序图、状态图、甘特图 | 简洁但可控性一般 | 支持导航与折叠 | 与 Markdown 系文档深度集成 |
| PlantUML | 中,需要记语法 | 完整 UML 图,特别是时序图、用例图、部署图 | 中和,可定制 | 弱 | 有专用插件,能和代码生成联动 |
| Graphviz | 中高,需要理解布局模型 | 依赖关系图、树状图、网络拓扑 | 强,布局算法成熟 | 弱 | 命令行体系,适合接入 CI |
| draw.io / Excalidraw | 零成本拖拽 | 复杂手绘风、自由布局、架构大图 | 高 | 强 | 支持文件直存 Git,但 diff 困难 |
2.2 各自的适用场景与选择逻辑
Mermaid 是我最常用的开图语言。如果一张图需要嵌在 Markdown 文档里,并且形式是流程、时序或者状态变化,我会优先选它。它的语法非常接近自然语言,一个流程从开始到结束写下来,基本不需要查文档。团队内部任何有基本代码阅读能力的人都能维护。选它的核心判断点是:图的复杂性适中,不需要细粒度控制节点位置。
PlantUML 则专注于 UML 体系。做架构评审时要表达完整的类关系、用例边界、组件部署,Mermaid 的能力就不够了。PlantUML 胜在 UML 支持面广,尤其是时序图里的激活块、组合片段这些语义,画出来非常规范。
Graphviz 是压轴武器。依赖关系复杂、节点数量动辄几十上百的图,用别的文本工具写出来往往是乱麻,但 Graphviz 内置的布局算法能自动把层次理顺。它的问题在于上手成本偏高,需要理解 DOT 语言的图模型,一旦熟悉了,处理知识图谱、依赖分析这类场景真的无可替代。
2.3 我实际项目中的最小配置组合
我的建议不是把一整套工具都铺开,而是按“主航道上少工具、边缘场景再扩展”的原则配置。我目前的组合是三件套:文档图用 Mermaid,架构与 UML 图用 PlantUML,复杂关系用 Graphviz。三者的输出全部落在同一个 docs 目录里,通过一套构建脚本统一渲染。
举个例子,一个微服务项目的架构文档结构大致是这样:
docs/ ├── architecture/ │ ├── service-deps.dot // 服务依赖关系,用 Graphviz 画 │ └── sequence-overview.puml // 核心调用时序,用 PlantUML 画 └── guides/ ├── onboarding.md // 新人指引,里面嵌 Mermaid 流程图 └── api-design.md // API 设计说明,嵌时序图这套组合的威力在于职责分明:新人引导这种高频阅读的图用 Mermaid,图小且渲染快;系统全局关系用 Graphviz 那种自动布局保证可读性;涉及规范建模时用 PlantUML。我见过不少团队“一套工具打天下”,结果所有图都长得差不多,表达不了更深的结构信息,实在可惜。
3. 一张好图是如何从需求到成图的:我的五步流程
3.1 第一步:先回答“这张图给谁看,要做什么决策”
这一步被我列为所有图表设计的第一原则,因为它决定了之后每一个选择。流程图给开发同事看和给产品运营看,颗粒度完全不一样;架构图给新入职同学看和给 CTO 汇报用,简化程度也完全不同。
我通常先问自己三个问题:读者是谁?读者需要在图里找到什么信息?读者拿到图之后是理解概念还是推进决策?如果答案是“推进决策”,那图里就要突出路径和结论,比如主流程加粗、失败分支弱化;如果答案是“理解概念”,那图里就要突出分层和归属,色彩用于分组而不是强调某一条路径。
以前我画图习惯直接开画,经常画到一半发现类型选错,比如用流程图去表达层级包含关系,效果非常别扭。后来养成了先回答这三个问题的习惯,几乎不会出现推倒重画的情况。
3.2 第二步:梳理节点与关系,而不是直接开画
确认清楚图和受众之后,先不碰画图工具,而是拿纯文本把所有要出现的节点列出来,再把所有关系列出来。节点用统一语义命名,比如“订单服务”“库存服务”“Redis”,关系用清晰的动词描述,比如“订单服务调用库存服务的扣减接口”。
这一步本质上是在画图之前先做数据结构设计。曾经给一个交易链路画时序图,开始直接在 Mermaid 里写,写了几行就发现自己把超时回调漏了,只能返工。后来每张图都先有这样一个节点清单:
节点:客户端、网关、订单服务、支付服务、消息队列、回调服务 顺序: 客户端 -> 网关:创建订单 网关 -> 订单服务:创建订单请求 订单服务 -> 支付服务:发起支付 支付服务 -> 支付服务:等待回调 支付服务 -> 消息队列:发送支付成功事件 消息队列 -> 回调服务:推送支付结果 回调服务 -> 客户端:返回支付结果这样处理之后,真正进入绘图语言时基本是机械翻译,很少再出现逻辑漏洞。
3.3 第三步:为关系定性:顺序、分支、循环还是依赖
很多人把关系一律画成箭头,这是图表可读性变差的重要根源。关系是有类型的,不同类型需要不同的视觉表达。我在这一步会把上一步的文本关系逐条标注类型:
- 时序关系:强调先后顺序,适合流程图、时序图,用普通箭头表达调用。
- 分支关系:强调条件判断,必须绘制判断节点,用不同的出边表示 true/false。
- 依赖关系:强调单向依赖,适合系统架构图,用虚线或实线表示不同的耦合强度。
- 循环关系:强调重复执行,需要在图中清晰标出循环边界,避免读者误以为是多个独立步骤。
- 包含关系:强调层级归属,适合组织结构、模块拆分,应该用嵌套或分区表达,而不是连线。
举个例子,画系统架构图最常犯的错误就是把所有服务直接用一条条带箭头的线连起来,看起来密密麻麻,信息却几乎为零。如果能把连线的语义分成“同步调用”“异步消息”“定时任务”“配置依赖”四类,并且用不同线型表达,图的信息密度和可读性立刻上升一个档次。
3.4 第四步:用最小表达完成初稿
明确关系后进入绘图编码阶段,我要求自己遵循一个原则:先求完整正确,再求美观,禁止一步到位追求好看。
具体操作是,第一版只在代码里写出所有节点和关键关系,不加任何样式、颜色、分组装饰。用 Mermaid 写就是只写节点和箭头,用 Graphviz 就是只写 node 和 edge,不碰 rank、颜色、shape。这样做的目的是让逻辑先自洽:节点有没有遗漏,路径是否走得通,边界条件有没有体现。
等逻辑确认无误,才进入样式层。很多新人喜欢一开始就调颜色和间距,结果逻辑有问题时返工成本极高,配色也白搭。我自己的习惯是至少过了两遍逻辑检查之后才动手加样式。
3.5 第五步:审查环节——至少能用“陌生人视角”看一遍
画完不等于完成。我会强制自己搁置几分钟,再以第一次看到这张图的“陌生人视角”通读一遍:不靠文字说明,单看图能不能理解全部信息?对三条原则做最终检查。
- 只保留必要节点:所有节点都在为传递核心信息服务,能删就删,能合就合。
- 路径是否清晰:从图的入口到出口,每一步是不是不需要回退就能读通。
- 样式是否信息一致:同样类型的节点样式是否完全一致;同样类型的关系线型是否完全一致。
这个阶段发现问题,修改成本很低,因为文本绘图改代码就行。但如果跳过这一步,渲染出的“成品”往往要到评审会上被同事问住才意识到问题,那时候改起来就很被动了。
4. 布局、颜色与语义:让图表“耐看”的三个硬规则
4.1 把阅读顺序画进图里
图表的可读性,本质上是读者能否快速找到阅读入口,并按照合理路径读完。我总结的规律是:单线流程从上到下,分支流程从左到右,复杂系统按主路径优先级排序,避免读者在海量节点里迷失方向。
Mermaid 默认的渲染方向是自上而下,PlantUML 时序图默认也是自上而下,这其实就符合大多数人的阅读习惯。Graphviz 里也可以通过 rankdir 参数显式指定方向,我画依赖图习惯用 LR(从左到右),因为依赖关系图天然是横向展开的结构。
确保阅读顺序还有一个常见的技巧:主路径放在画布中部或者最显眼的位置,辅助路径、异常分支放到边缘位置。用 Mermaid 时,可以把核心节点写在代码前部,渲染出来的布局通常更靠上或靠左,这能实际影响视觉重心。
4.2 颜色控制在三个语义层次以内
颜色是图表中信息密度最高的视觉变量,也是最容易失控的地方。我见过太多架构图把每个盒子刷成不同颜色,五颜六色交代不了任何系统信息,反而增加了认知负担。
我的硬规则是:同一张图里,颜色语义层级不超过三个。
| 语义层级 | 用途 | 示例 |
|---|---|---|
| 结构色 | 标识节点类型或模块归属 | 所有外部依赖一个颜色,内部服务另一个颜色 |
| 状态色 | 标识状态变化或优先级 | 正常路径绿色,异常路径红色,可延迟处理黄色 |
| 强调色 | 标记本次变更或核心链路 | 全图只有重点路径用高亮色,其他全部弱化 |
这个表格在实践中给过我巨大帮助。曾经画一张促销系统的活动状态机,一开始给每种状态都分了一个颜色,画完整个图像调色板。后来强制收敛成三色:未开始、进行中、已结束分别用灰、蓝、绿,特殊异常用红,图立刻清爽,评审会上也不再有人问“这个浅橙色是什么意思”。
4.3 命名规范:节点名称就是接口约定
节点名称是读者第一个接触的信息面,命名不统一会极大增加理解成本。我要求自己遵循几个简单规则:
- 同一张图里同一个概念绝对不允许出现两个名字。产品经理口中的“订单”、开发文档里的“TradeOrder”,在图中必须统一下来。
- 名称用“业务名词 + 层级词”来组织。比如“订单服务-API”“订单服务-消费者”“订单服务-定时任务”,比“订单”“订单消费者”“任务”这种模糊称谓清晰得多。
- 动词关系统一:如果要表达调用,全部用“调用”,不要一会儿“访问”一会儿“请求”。英语环境同理,不要 call/invoke/request 混用。
刚开始执行这些规则会有点用工,但一旦形成规范,图中每个节点的含义就稳定下来。时间一长,这些节点名称几乎成了团队内部的沟通术语,沟通效率和图表本身的质量一起提升。
4.4 布局上减小连线交叉的实用手法
连线交叉是图表可读性的头号杀手。文本绘图没法像手绘那样自由拖动节点,但只要掌握几个原则,依然能把交叉控制在可以接受的范围。
第一,同类节点相邻放置。把逻辑上相近的节点在代码里写在一起,渲染布局通常会更加聚合,减少跨区域的连线。第二,用中间节点降低交叉。A 和 D 之间长距离直连容易穿越其他节点,通过中间业务节点中转,虽然多了一个框,但图更显清晰。第三,利用分组替代连线。Graphviz 中可以把多个子节点放进同一个 subgraph,Mermaid 里用 subgraph 或容器表达层级,把“包含关系”从连线改成嵌套,交叉自然消失。
如果是特别复杂的关系网络,与其纠结布局,不如直接考虑把图拆成多张。一张图放不下的信息,就分视角描述,比如总体集成关系一张图,核心链路详图再一张,靠文档里的跳转把它们串起来。这在真实工程中比一张“超全”的巨型图有效得多。
5. 图表纳入版本管理与持续渲染:团队协作的进阶玩法
5.1 图和代码一起提交,别让文档滞后
图表自动化链路里最值得投入的一环,就是把图源文件和代码一起提交。设想一个场景:代码评审时新增了一个异步消费逻辑,如果提交里同时包含时序图的更新,评审人可以直观理解新逻辑的时序关系。相反,如果图在代码合并两周后才手工更新,那时序已经错得面目全非。
现在我的团队约定是:如果一个代码变更涉及核心链路、模块依赖或接口协议,那么提交信息里必须包含相应图表的更新。这听起来严格,执行成本其实很低,因为时序图之类是文本源文件,改动量通常十几行;但换来的收益是文档永远和代码同频,团队里再也没有“过时文档到处误导人”的问题。
5.2 CI 中自动渲染导出图片
图源文件存在于仓库里只是第一步,让渲染产物自动生成才能解除对本地环境依赖。我的方案是用 GitHub Actions(或同类 CI)监听 docs 目录的变更,把 Mermaid、PlantUML、Graphviz 的源文件批量渲染成 SVG 和 PNG,输出到指定目录。
下面是一个针对 Mermaid 和 Graphviz 的渲染示例:
name: render-diagrams on: push: paths: ['docs/**'] jobs: render: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Render PlantUML diagrams run: | java -jar plantuml.jar -tsvg docs/**/*.puml - name: Render Graphviz diagrams run: | for f in $(find docs -name '*.dot'); do dot -Tsvg "$f" -o "${f%.dot}.svg" done - name: Commit rendered outputs run: | git add docs/ git commit -m "chore: render updated diagrams" || echo "No changes"这样改图源文件后,渲染产物自动出现在仓库,在线文档直接引用 SVG 路径即可,永远展示最新版。我自己的体验是:这条流水线跑通后,团队对文档图的维护积极性提高了一个层次,因为“改完图会自动更新”的反馈足够即时。
5.3 让图表 diff 成为评审的一部分
代码评审习惯已经普及,但图表 diff 仍未成为团队标配。其实图和代码一样需要评审,评审的重点不是美观,而是变更是否合理:新连接是否反映了真实的依赖?移除的节点是否真的不再需要?
文本图表让这个评审过程非常流畅。打开 PR 的 diff 页面,可以看到:
- order-service --> payment-service : 发起支付 + order-service --> payment-service : 创建预支付订单 + payment-service --> order-service : 异步回调这种逐行变更的清晰度是图片评审完全做不到的。我还养成了一个习惯:PR 描述里放上一张变更前后的渲染对比图,评审者一眼就能看出结构差异。既能看到意图,又能逐行审查,图表评审就不再是走过场了。
6. 踩坑记录:文本绘图最常遇到的五个问题
6.1 中文字体和乱码问题
文本绘图工具大多最初为英文设计,中文渲染出来要么乱码,要么字体难看。Graphviz 用系统默认字体渲染中文时经常出现方框, Mermaid 默认的字体对中文支持还好,但导出 PDF 时偶尔也会出问题。
我的建议是把中文字体设置写进全局配置一次,之后就不用再管。Graphviz 里可以在配置里统一指定字体名,PlantUML 也支持类似的字体配置。如果团队文档完全使用中文,建议直接选择一个中文字体配置存为模板。顺便提一下,命名时尽量使用中文语义清晰化,不要为了规避字体问题用拼音。
6.2 图一复杂就挤成一团
文本绘图最容易出现的问题就是:写的节点越多,渲染出来的图越密,直到挤成一团没法看。这其实不是工具的问题,而是维护者没有养成“复杂度阈值拆分”的意识。
我的经验法则是:单张流程图节点超过 12 个,时序图 participant 超过 8 个,依赖图超过 30 个节点,就得考虑拆分或分层表达。拆图不是偷懒,而是用多视角替代单张巨图。总览图只画核心模块,各模块细节图单独一张,通过文档链接互相引用,阅读体验远好于一张巨型图死磕到底。
6.3 多人并行编辑同一张图
文本图可以走 Git 合并,但合并体验比代码差很多。图中一行节点名的变更就可能引起大段布局重排,多人同时改一张图时冲突非常高。
我采取的策略是:给“共享图”指定负责人,其他人有改动需求时以需求形式提交而不是直接改。变更比较独立的图可以自由修改,涉及全局结构调整的图必须通过负责人统一协调。这么做不是限制协作,而是减少无意义的合并冲突成本。
6.4 导出大小和清晰度不可控
默认导出的 SVG 或 PNG 经常要么太大要么太模糊。SVG 是矢量格式,适合清晰的文档插图;而默认的 PNG 分辨率可能不够出版或打印要求,图片还会带着多余白边,嵌入文档时看着很不舒服。
我的统一处理方案:文档内正常使用 SVG,需要位图时用 SVG 转高分辨率 PNG(2 倍或 3 倍),再用裁剪工具去掉白边。这个环节做成脚本跑一遍,避免手工操作。用户体验上,SVG 可缩放、体积小,几乎适合所有在线文档场景。
6.5 过度美化导致维护成本飙升
有些人一旦尝到样式定制的甜头,就开始给每张图加主题色、描边、阴影、圆角,图确实变好看了,但维护成本也上来了。尤其是每个人都按自己的偏好去改样式,图表库很快就变成一个混乱的样式堆。
我的教训是:定义一套团队统一的图表面板,预设配色和线型规则,任何人画图都从这套面板出发,不允许个性化自定义。把审美偏好收敛成规则之后,图的风格统一了,迁移维护成本也下降了。想调整风格时,只改面板和主题文件,所有图一次性更新,这在拖拽工具里根本做不到。
7. 复盘模板与个人检查清单:从能画到画好
7.1 我的图后复盘三轮法
画完一张关键图表后,我会给自己安排三轮复盘,分别从逻辑、结构和呈现三个层面过一遍。
第一轮看逻辑对错:关系有没有画反,边界条件有没有体现,节点是否全部真实存在。这轮必须对着真实系统或代码核验,不能只凭记忆。第二轮看结构效率:节点能不能合并,路径有没有冗余,现有最佳画法下这张图能不能再简化。第三轮看表达一致性:同样的术语、同样的线型、同样的颜色,是否符合既定语义,会不会让读者产生歧义。
这个方法听起来机械,但坚持半年以后,我画图的返工率下降非常明显。尤其是第三轮,因为一致性最能体现图表的专业度,也最容易被忽略。
7.2 一张图上线前的 10 条自查清单
下面是我现在每张图在提交前的固定检查项,以表格形式分享出来,可以直接抄走当团队清单。
| 编号 | 检查项 | 说明 |
|---|---|---|
| 1 | 读者明确 | 知道这张图给谁看,解决什么问题 |
| 2 | 类型合适 | 流程图、时序图、结构图没有错配 |
| 3 | 节点完整 | 核心节点没有遗漏,边界条件有体现 |
| 4 | 关系正确 | 每条边的方向、语义都和实际系统一致 |
| 5 | 命名统一 | 同一概念全图只有一个名称 |
| 6 | 颜色有语义 | 颜色不超过三层,且每层含义清楚 |
| 7 | 主路径清晰 | 核心链路一眼可辨,非核心路径不喧宾夺主 |
| 8 | 交叉可控 | 长线穿越节点的情况已经优化 |
| 9 | 源文件入库 | 图源代码已经提交,和代码在同一次变更里 |
| 10 | 构建通过 | 渲染产物自动生成,链路无报错 |
这份清单打印出来贴在显示器边,画图时对照一过,能在提交前拦掉绝大部分常见问题。
7.3 最后的一点个人体会
图表设计做到后面,技术反而是最简单的部分,真正的难点在于克制。克制加更多节点表达“全面”,克制用更多颜色表达“丰富”,克制在一张图里塞进所有信息表达“完整”。好的 diagram-design 永远是把最少的元素组织成最清晰的逻辑链。
我自己也是在画废了无数张“全面丰富完整”的大图之后,才学会做减法。现在每次打开绘图文件,都会先沉住气把信息结构梳理清楚,再动手写第一个节点。过程慢了一点,但图的生命周期长了很多。希望这套经验对你也有用,下次设计图表的时候,不妨先放下工具,拿出一张纸想想这张图到底要说什么。