做后端开发的第三年,我终于受不了文档里那堆东拼西凑的破图了。架构说明里一张时序图画得歪歪扭扭,业务逻辑改了两次,图却忘了同步,新同事接手时对着旧图猜逻辑,浪费了整整一个下午。后来我把“画图”这件事认真当做一个设计问题来处理,慢慢折腾出一套完整的方法,也就是这个项目标题里写的“diagram-design”。
这里的“diagram-design”不是指某个现成的绘图软件,而是一套面向工程师的图表设计方法:用代码工具把架构图、流程图、时序图当成工程文件来管理,配合一套统一的样式规范,让图表真正跟得上代码的版本演进。它适合经常要写技术方案、做技术汇报、维护项目文档的开发者,也适合团队里负责对外输出技术内容的同学。这篇文章把我这几年的实践思路、踩过的坑以及最终沉淀下来的操作流程,一次性整理出来。
1. 项目定位:先弄清要画什么,再决定怎么画
很多人在做图表设计时犯的第一个错误,不是工具用得不好,而是压根没想清楚这张图要解决什么问题。同样叫“图”,架构图、流程图、时序图表达的完全是不同维度的信息,混用之后,读者看着累,作者自己也说不清。
1.1 画图前必须回答的三个问题
我在动手画任何一张图之前,都会先问自己三个问题:这张图给谁看,图中要表达的核心关系是什么,以后多久会改一次。
给谁看决定了图的抽象层级。给老板看,就要弱化技术细节,突出模块边界和数据流向;给接手开发的同事看,就要保留必要的服务名、接口名、关键参数。同样是订单模块,两种读者的图可以完全不同。
要表达什么关系,决定了图型。系统的组成部分和层级关系,用架构图;业务处理流程的分支走向,用流程图;多个对象之间的交互顺序和时间关系,用时序图;数据模型的字段与关联,用ER图。关系选错了,工具再熟练也救不回来。
多久改一次,决定了你值不值得投入成本去做自动化。如果一张图一年只画一次,手工拖拽完全够用;如果跟着敏捷迭代每两周就要更新一次,那必须把图变成代码文件,才能低成本地持续维护。
1.2 四种高频图型的正确打开方式
我日常用到的图型,绝大多数可以归纳为四类,它们的适用边界非常清晰。
| 图型 | 表达核心 | 典型场景 | 常见误区 |
|---|---|---|---|
| 架构图 | 系统组成与层级关系 | 服务拆分、模块划分、部署拓扑 | 把部署细节混进逻辑架构 |
| 流程图 | 业务逻辑与判定分支 | 订单状态流转、异常处理、审批流程 | 忽略异常分支,只画理想路径 |
| 时序图 | 跨对象交互顺序 | 接口调用链、登录认证、分布式事务 | 把调用层级画成包含关系 |
| ER图 / 类图 | 数据与对象结构 | 数据库设计、领域模型建模 | 字段过多导致可读性崩塌 |
架构图的核心是“分层”。我用它表达系统由哪些部分组成、每个部分属于哪一层、层与层之间如何交互。这里最容易犯的错是,有人会把一台服务器上的IP、端口、中间件版本全塞进架构图,结果逻辑关系和部署关系混在一起,图瞬间就废了。架构图管的是逻辑边界,部署细节应该交给拓扑图。
流程图的核心是“分支”。有些人画流程图特别潦草,只画主流程,遇到条件判断只写一个分支,另一个分支藏着不画。这在文档评审里就是埋雷,因为所有线上问题几乎都出在异常分支里。正确的做法是每个菱形判断下面至少画两条路径,正常路径和异常路径都落到具体节点。
时序图的核心是“顺序”。它最有价值的地方在于能清楚地看到谁先调谁、谁等谁的结果。画时序图的时候不要把消息画成嵌套关系,不要把系统内部的方法调用画成服务间通信,否则交互链路会完全失真。后面我给出的实操例子会专门展示这一点。
2. 工具选型:为什么代码驱动更适合工程级图表
工具选型这件事,很多人是随便挑一个就上手了。在方案设计阶段用在线白板随便画画没问题,但一旦图表要进入仓库、跟随版本、多人协作,拖拽式工具就会暴露出一堆致命问题。
2.1 拖拽画图的三个隐形代价
第一个代价是“图粘连”。用拖拽工具画完图,导出成PNG,图片就是一张死图。下次逻辑改了,你对着旧图改无可改,只能重新画。如果间隔时间超过三个月,你甚至看不懂自己画的线为什么连到那里。
第二个代价是“不可评审”。代码可以用Code Review逐行看差异,拖拽出来的图本质上是一个二进制文件或私有格式文件,两个人同时改一张图几乎一定会冲突。评审时只能看最终效果,改了什么、为什么改,完全没有记录。
第三个代价是“风格失控”。三个人维护同一份文档,用不同的画图工具,出来的配色、线宽、字体五花八门。读者一眼就能看出这是拼凑出来的文档,信任感直接下降。我自己曾经见过一份技术方案里六张图五种配色风格,看着就很头疼。
2.2 四类代码绘图工具的能力边界
代码驱动不是指你用代码去“画”像素,而是用文本描述图的结构,由渲染引擎自动生成布局。主流方案里我用得比较多的是这几类。
Graphviz使用DOT语言,特点是布局算法强大,特别适合有清晰层级结构的架构图和依赖图。缺点是所有节点位置基本交给算法,如果你对布局有强迫症,需要花不少心思通过rank、constraint这类属性去约束。
PlantUML用文本描述时序图、用例图、状态图,读起来几乎接近自然语言,团队上手成本极低。时序图它的支持最舒服,消息顺序、激活状态、返回消息都能清晰表达。类图和ER图它也覆盖,我一般都用它画数据模型。
Mermaid最常见的使用场景是Markdown文档内嵌,在笔记软件和技术社区里渲染非常方便。它的优点是零成本,写完即展示,但复杂排版的掌控力偏弱,节点稍多就容易挤成一团。我对它的定位是备忘草图,而不是正式交付物。
另外还有一类新兴声明式工具如D2,语法干净,输出质量也不错,适合愿意尝试新工具的人。我建议团队选型时固定一两个主力工具,不要每张图都用不同语言,否则维护成本会成倍增加。
2.3 我的选型组合建议
我现在的固定组合是这样的:架构图和ER图用Graphviz,时序图和状态图用PlantUML,临时草图用在线白板或Mermaid,最终交付统一导出SVG。为什么这么分?因为Graphviz对层级布局的算法最成熟,架构图的分层结构正好是它的主场;PlantUML对时间顺序的语义表达最清晰,时序图这种有明确先后顺序的关系是它的强项。
选择工具还有个隐藏标准——渲染是否可离线、是否可脚本化。我在自动化流程里会把“画图”这个动作嵌进构建脚本,所以渲染器必须支持命令行执行。这也是我最终放弃只依赖在线工具的根本原因。
3. 完整实操:用代码把图表做成工程文件
定义好图型、选定工具之后,真正上手画图其实是有固定套路的。我拿一个虚拟的“某电商系统订单模块”和“用户登录流程”两个例子,把从需求到成稿的完整过程走一遍。
3.1 架构图落地:用DOT语言画订单模块
需求是这样的:订单模块由Web网关、订单服务、库存服务、支付服务组成,数据层有订单库和缓存。我的第一反应是确定分层,把网关放接入层,业务服务放业务层,库和缓存放数据层,然后用subgraph把同层节点聚合起来。
digraph order_system { rankdir=TB; node [shape=box, style="rounded,filled", fontname="Microsoft YaHei"]; subgraph cluster_web { label="接入层"; style=dashed; web [label="Web 网关"]; } subgraph cluster_biz { label="业务层"; style=dashed; order [label="订单服务"]; inv [label="库存服务"]; pay [label="支付服务"]; } subgraph cluster_data { label="数据层"; style=dashed; db [label="订单库"]; redis [label="缓存"]; } web -> order; order -> inv; order -> pay; order -> db; order -> redis; }这段代码有几个关键点。rankdir=TB表示整体自上而下,符合分层架构的阅读习惯。三层业务逻辑分别放在三个cluster子图里,Graphviz会自动给子图加虚线框,这样层与层之间的边界一目了然。
node这一行统一定义了所有节点的默认样式,shape=box配合rounded是圆角矩形,filled让节点内部填充颜色。如果节点多,每个都要单独写样式会很啰嗦,所以我习惯把所有通用样式放在node的默认属性里。
绘制完成后执行dot命令导出成SVG矢量图:
dot -Tsvg order_system.dot -o order_system.svgSVG的好处是无限放大不模糊,可以直接用来插入技术文档。我个人的习惯是收到SVG之外,再导一份PNG版本放在图片目录里,方便不能预览SVG的同事直接看图。
3.2 时序交互落地:用PlantUML画登录流程
架构图解决的是“有哪些子系统”的问题,时序图解决的是“这些子系统之间怎么协作”的问题。以用户登录为例,涉及的参与者有用户、前端页面、认证服务、用户库。整个交互链路是用户提交凭证、认证服务验库、返回令牌、前端存储并跳转。
@startuml actor 用户 participant "Web前端" as web participant "认证服务" as auth database "用户库" as db 用户 -> web: 输入账号密码 web -> auth: 登录请求 auth -> db: 查询用户 db --> auth: 返回用户凭证 alt 凭证校验通过 auth --> web: 返回访问令牌 web --> 用户: 登录成功并跳转 else 校验失败 auth --> web: 返回错误码 web --> 用户: 提示账号或密码错误 end @enduml画时序图有两条铁律。第一,箭头方向必须和调用方向一致,请求用实线,返回用虚线。PlantUML里->是同步请求,-->是返回,这套语义约定能避免很多线头线尾画反的情况。
第二,把交互画平。时序图的精髓是把跨对象的调用摊开成一条时间轴,不要把认证服务的内部逻辑画进去,否则读者会误以为前端直接调用了数据库。像alt这样的条件分支,我通常只画“成功”和“失败”两条路径,再复杂的分支就拆成另一张流程图去表达。
PlantUML可以用命令行直接渲染,也可以用官方在线服务,但我会把源文件放在仓库里。渲染命令很简单:
plantuml login_flow.puml -tsvg3.3 全局样式统一:一份设计Token接手所有图
工程化图表体系里,比单张图画得漂亮更重要的是所有图长得像同一个体系的产物。我借鉴前端样式方案里的Token思路,把颜色、圆角、字体这些设计变量抽到一个公共配置文件里,所有图都从这份配置取参数。
{ "colors": { "primary": "#2563EB", "ok": "#16A34A", "warn": "#D97706", "error": "#DC2626", "neutral": "#64748B" }, "node": { "fill": "#F8FAFC", "border": "#94A3B8", "radius": 4, "padding": "8px 12px" }, "font": { "family": "Microsoft YaHei", "size": 12 }, "line": { "width": 1, "arrowSize": 0.8 } }为什么要花这个功夫?举个例子。订单架构图里的“订单服务”节点如果不是重点,就用中性色;如果这次改造的核心是订单服务,那我可以把它的填充色改成主色来突出。颜色代表语义而不是装饰,这套规则一旦固定,任何新同事加入都能快速产出风格统一的图。
Graphviz里可以在DOT文件顶部通过include引入公共样式片段,PlantUML则通过!include引入。这样,改一次Token配置,所有图都能同步更新,省去了逐张手工调整颜色的重复劳动。
4. 排版配色与可读性:让图表看起来像同一套设计
代码能跑不代表图好看,更不代表图易读。排版和配色是图表设计里最容易被技术人忽略、却又最能拉开专业感差距的部分。
4.1 给颜色赋予语义,而不是当装饰
我的配色原则很简单:一张图里,颜色数量控制在四到五种以内,每种颜色承担明确的语义。主色用来标识核心模块和主要流程,成功色用于正常路径的正常状态,警告色用于重试、降级或需要注意的中间态,错误色用于失败路径,中性色用于弱化辅助信息。
这样读者看到颜色就能立刻抓住视觉重心,而不是先把图例背下来再回去找。拿订单架构图举例,正常路径的调用线用主色,支付失败的分支用错误色,异步补偿操作用警告色,一眼看过去主次分明。
这里特别提醒一下,尽量不要滥用透明度、渐变和阴影。图表是信息载体,不是海报。渐变和阴影在印刷和投影场景下都可能出现渲染偏差,而且会让节点之间的层级对比弱化。用扁平填充加点细边框,是最稳妥也最耐看的方案。
4.2 布局、间距与连线的细节规则
布局细节上我有几条固定习惯。
箭头方向必须全局统一,要么全部从上到下,要么全部从左到右。Graphviz里通过rankdir控制,PlantUML时序图天然有上下时间轴,不需要额外设置。混用方向在各式图形里会迫使读者来回转动头部,非常影响阅读节奏。
节点间距要适当。Graphviz的nodesep和ranksep两个参数,我通常设置为节点本身尺寸的三分之一以上。间距太密,线头会和相邻节点粘在一起;间距太大,图会显得松散。合适的间距是“能看清箭头而不至于觉得空”。
连线尽量走正交路径。Graphviz默认的splines在节点多时会产生大量弯折,我一般设splines=ortho让连线保持横平竖直,视觉上会干净很多。正交连线还有一个隐藏好处——读者能顺着线段快速找到对应目标,不会在交叉线里迷路。
文字处理方面,中文环境务必显式指定中文字体。Graphviz的fontname属性如果不设中文字体,渲染出来的是方块乱码。另外,给节点命名时用字母ID,展示时用label属性写中文,这两者分离可以避免因为文件名带了中文导致的脚本兼容问题。
4.3 多图一致性的维护机制
单张图画得再漂亮,也只是第一步。真正难的是让文档里二十张图长期保持统一风格。我的做法是把设计Token和绘图源文件统一放在仓库的diagrams目录下,维护一份README说明选型规则和新增图表的操作流程。
团队协作时,约定是:新增图之前先看已有图例,优先复用已有工具链和Token,不要为了画一张小图去引入一个新的在线工具。建立图表审核机制,在代码评审里把“图的逻辑是否和实现一致”作为一个检查项,而不是只看有没有图。
如果团队成员对图表工具的语法不熟,我还会维护一份最小可用的示例模板,每种图型一个文件,新人直接用模板改内容,不需要从头学语法。这套机制落地之后,我见过最多的情况是从源文件到成品只要十分钟,风格还和团队已有图完全统一。
5. 高频问题排查与避坑手册
实践几年下来,我几乎把所有能踩的坑都踩了一遍。这一节整理成问题排查实录,每一条都对应真实场景,可以直接抄作业。
5.1 中文乱码与字体渲染
Graphviz中文乱码基本是字体问题。Windows环境我在node默认属性里加fontname="Microsoft YaHei",macOS用fontname="PingFang SC",Linux服务器上要提前安装中文字体包,否则DOT里指定了也没用。
PlantUML的中文支持需要通过skinParam设置默认字体:
skinparam defaultFontName "Microsoft YaHei"还有个隐藏坑是服务器上跑定时渲染时,因为服务器没装中文字体,本地正常的脚本在服务器上输出乱码。这个问题的排查顺序是:先确认渲染机器有字体,再排查配置项。我现在会在部署脚本里加入字体检查这一步,不通过就直接报错,避免生成一堆废图。
5.2 导出模糊与格式选择
很多同事习惯直接导出PNG,然后在Word里把图片拉伸,结果文字和线条全部变糊。我的建议是正式文档一律用SVG矢量图。对于Markdown技术文档,SVG在小尺寸下显示会较窄,可以配合指定宽度展示,或者额外导出双倍尺寸的PNG做兼容。
如果真的需要位图,导出的DPI不要低于192。Graphviz用-dpi参数,PlantUML用-scale参数。我实测下来,192 DPI在Retina屏上勉强清晰,普通屏幕也可以接受。如果印刷,建议直接给到300。
5.3 复杂图表布局失控
节点超过二十个时,无论是Graphviz还是PlantUML,布局都会变得难以控制,线乱成一团。这往往不是工具问题,而是图的设计出了问题。
我的处理办法是拆图。一张架构图画不下所有细节,就拆成总体架构图加模块详图。总体图控制节点数量,只保留关键服务和连线;详图聚焦一个模块,把内部实现画清楚。文档里用“总体图+章节详图”的结构互相引用,读者既能快速建立全局认知,又能在需要时查看细节。
另一种常见做法是用subgraph或package把相关节点聚合起来,减少跨区域的连线数量。跨区域的长线是布局混乱的最大来源,分组能有效把长线变成短线。
5.4 团队协作与版本管理
图表源文件纳入Git仓库之后,会出现一个棘手问题:代码Review里根本看不出图变成什么样,因为源文件的diff对不熟悉语法的人来说很抽象。我的解法是写一个轻量渲染脚本,在本地生成图片并提交,或在CI流程里自动渲染并附到评论中。
这里还要注意并发编辑冲突。两个分支同时改同一个DOT文件,合并时冲突通常难以解决。我的建议是保持单一事实源,一个图尽量只有一个人负责维护,其他人提修改建议而不是直接改文件。如果必须多人协作,就需要明确分工,按模块拆分成多张图而不是挤在同一张图里。
5.5 问题速查表
| 问题 | 现象 | 原因 | 解法 |
|---|---|---|---|
| 中文乱码 | 节点文字变方块 | 未指定中文字体 | 设置fontname/fontName |
| 图片模糊 | 放大后线条毛糙 | 位图分辨率不足 | 用SVG或提高DPI |
| 线条交叉 | 图乱如蜘蛛网 | 节点过多、缺少分组 | 拆图、用subgraph聚合 |
| 节点重叠 | 文字被覆盖 | 间距参数过小 | 调nodesep/ranksep |
| 字体不一致 | 各图字体不同 | 每张图单独设字体 | 引入设计Token统一 |
| 修改不生效 | 渲染图还是旧版 | 缓存或改错源文件 | 确认源文件路径并清理缓存 |
排查这类问题我一直坚持一个原则:先看源文件结构,再怀疑渲染环境。大多数看似奇怪的现象,根源都在数据和配置,而不是工具本身。
最后再分享一点私人经验。图表设计这套方法,最核心的收获不是哪张图画得漂亮,而是一套让所有图都保持整齐、可维护、可演进的工程框架。代码会重构、业务会变化,文档里的图如果不跟着代码走,迟早变成误导。把图当代码管理,用Token统一风格,用拆图控制复杂度,这三件事做到位,图表文档就能一直“活”到项目最后一天。如果团队里还没人建这套规范,你可以从一张架构图开始,慢慢把流程带起来。