刚把最近一套 diagram-design 的实践心得整理完,忍不住想聊几句。很多人一听到画图就觉得是“找个工具拖一拖、连几根线”的事,可真到架构评审、方案汇报的时候,一张没经过设计的图能把所有人拖进歧义里。我自己踩过这个坑,才发现 diagram-design 不是顺手涂鸦,它是一套关于信息编排、视觉层级和语义表达的完整方法,值得我们把每个细节都抠清楚。
这篇文章我准备写给所有需要在文档、评审、技术方案里放图的人:后端工程师、前端工程师、架构师、产品经理,甚至做数据分析和知识管理的朋友。内容的核心就一件事——怎么让你画的图又快又清楚,既能表达复杂关系,又不会让读者产生错误理解。我会从“图为什么会乱”讲起,把工具选型、信息层级、色彩布局、代码化自动化这些环节逐一展开,最后分享一些实际排查中积累的经验,都是踩过的坑换来的。
1. 先搞清楚 diagram-design 到底在解决什么问题
1.1 一张没设计的图是怎么毁掉一场评审的
我印象很深的一次方案评审,同事贴了一张系统资源图,节点大概有二十多个,箭头颜色有五六种,线有实线有虚线,有人还在上面标了蓝色批注。结果评审会的前十分钟,大家一直在争论“这条虚线到底表示定时任务还是异步消息”,还有人把蓝色批注误当成缺失的模块。最后架构师只能打开代码仓库现查逻辑。一场本该讨论性能指标的评审,变成了看图猜谜。
这个场景并不少见。之所以会这样,是因为画图的人默认“线就是关系”,但没有定义是什么关系;默认“颜色是装饰”,但没有赋予颜色固定语义;默认“图能表达一切”,却没有考虑读者的阅读路径。Diagram-design 的第一层价值,就是逼着你在画图之前先做取舍:哪些信息必须保留,哪些信息可以删掉,哪些关系适合用空间位置表意、哪些必须靠箭头和标签来讲清楚。把这些想明白,图才开始具备“交流能力”。
1.2 diagram-design 的本质是信息编排,不是画图
有一句话流传很广:一图胜千言。但它成立的前提是,这张图是经过编排的。我更喜欢把 diagram-design 理解成“把一段复杂逻辑翻译成二维空间语言”的过程。这个翻译过程里,你需要决定节点如何摆放、层级如何划分、连线如何路由、文字如何标注,甚至要决定整张图的阅读起点在哪、视觉终点落在何处。
举个最简单的例子:同样是画三个服务之间的调用关系,A 服务在下、B 服务在中间、C 服务在上的布局,和把三个服务排成一行的布局,传递的信息重心完全不同。前者暗示了上下层依赖,后者更像是并列流程。如果再加一个人坚持把所有接口名都写在箭头上,另一个人只写“HTTP”,这两种图表达的信息密度也不一样。所以 diagram-design 本质是内容策划工作,工具只是最后落笔的载体。
1.3 不同图种的“设计重心”差别很大
不是所有图都叫架构图,diagram 这个英文词是一个大类。在实际工作里,我至少会区分四种常见图,它们的设计重心完全不同。
架构图重点关注模块边界、层次关系和数据流向。这类图最怕信息爆炸,一个框里塞二十个服务名,还不如直接给链接;流程图重点在于分支条件和循环路径,它的设计核心是“读者能不能顺着主干一路走到底”;时序图则强调消息先后顺序和对象之间的交互,时间轴清晰比什么都重要;数据知识类图更关注关系类型、基数约束和层级归类。很多人在画时序图时用架构图的设计习惯,结果整张图没有时间轴感,交互顺序全靠猜。理解图种差异,是 diagram-design 落地的第一步。
2. 图表工具怎么选:从自由画布到代码描述的三条路线
2.1 自由画布类工具:上手最快,但风格约束是短板
自由画布工具的代表有 draw.io(现在叫 diagrams.net)、Excalidraw、FigJam、ProcessOn 之类的。它们最大的优点是“所见即所得”,拖拽节点、连线条,小白五分钟就能画出一张像样的图。
但这类工具也有一个明显问题:自由意味着不统一。同一个团队里,有人喜欢圆角矩形,有人用直角,线条颜色能凑成一道彩虹。我见过有团队用 draw.io 画架构图,最后每张图的字体大小都不一样,导出图片的缩放比例也不统一,放在文档里像拼贴画。所以如果用自由画布工具,一定要配套一个团队模板,把节点样式、配色、字体、箭头类型固定下来,否则图和图之间根本没有一致性。适合自由画布工具的典型场景是:快速原型讨论、白板头脑风暴、对布局精细度要求不高的内部草图。
2.2 代码描述类工具:用文本画图,让图保持可追溯
代码描述类工具我日常用得最多,主要有 PlantUML、Mermaid 这类。它们的核心思路是拿文本定义节点和关系,再通过渲染引擎生成图片。这么做的好处很实在:图可以做版本管理、可以参与代码评审、可以自动生成,也可以嵌进 Markdown 文档里。
代价是布局可控性有限。你没法像在画布工具里那样,随手把一个节点拖到某个位置。所有位置都是自动布局引擎决定的。这在图很复杂的时候会变成灾难:比如二十个节点互相连线,自动布局可能会把线绕成一个蜘蛛网。我的建议是,代码类工具适合结构相对规则、逻辑清晰的图,比如流程图、状态图、类图、简单架构图。它不适合需要精细控制大量节点绝对位置的复杂系统图,硬要用也不是不行,只是布局调整的时间成本会高得离谱。
2.3 可编程绘图方案:适合把图当作产品的一部分
第三种路线是相对小众但非常有力量的一类,比如用 D2、Graphviz、Diagrams(Python)这类可编程方案。它们和代码描述类工具的区别在于,你不仅可以描述节点和关系,还能通过编程逻辑动态生成图。举个例子,如果你有一个服务依赖清单,希望每周自动产出一张最新版架构图,用 D2 或者 Python 脚本接上清单数据,就能做到“图跟着数据走”。
我推荐把这一类划入“工程化绘图”路线。它的设计目标不是让某一张图多漂亮,而是让图成为可维护的工程资产。使用场景很明确:系统复杂度高、图需要频繁更新、对一致性严格要求。它不适合一次性的临时草图,因为初始化成本和学习成本比前两类高。
2.4 工具选型建议:别迷信单一方案
我自己的实践里,从来没有“一个工具打天下”的说法。我的建议是组合使用:临时沟通用 Excalidraw,正式文档优先用代码类工具,复杂架构图落在 D2 或 Graphviz 上,需要精细排版时再用 draw.io 手工调整导出。下表是我常用的选型逻辑,可以给大家参考。
| 工具类型 | 代表工具 | 适合场景 | 不太适合的场景 | 上手成本 |
|---|---|---|---|---|
| 自由画布 | draw.io / Excalidraw / FigJam | 快速草图、头脑风暴、自由排版 | 需要版本管理、团队风格统一的正式文档 | 低 |
| 代码描述 | Mermaid / PlantUML | 文档内嵌图、流程简单清晰、自动化渲染 | 复杂拓扑、对布局有强要求的图 | 中 |
| 可编程绘图 | D2 / Graphviz / Diagrams | 复杂架构、动态数据驱动、CI 集成 | 临时的简单草图 | 中高 |
一个小提醒:切换工具是有代价的。项目越往后,历史图的存量越多,迁移成本越高。所以建议大家尽量在项目初期就定好主工具,把样式模板一起定下来,而不是画到一半频繁换工具,最后所有图都长着不同的“脸”。
3. 真正让图“好看又好懂”的核心:层级、网格与视觉动线
3.1 信息层级:用视觉权重告诉读者先看哪里
这是我画图时最看重的一点。一张图的信息层级,决定了读者扫一眼之后,是抓住了核心结构,还是迷失在边角细节里。信息层级的控制手段不多,但非常有效:大小、粗细、颜色和留白。
大节点天然比小节点更吸引注意力,深色和粗线框比浅色细线框更重,饱和度高的颜色会比灰色更跳。如果一张图里所有节点同等大小、同等粗细、同一颜色,那它就没有层级,只有一堆并列方块。实际操作中,我会把核心服务放大一号、加深描边,把辅助节点缩小三分之一、降低饱和度;虚线框和浅色背景一般用来表达逻辑分组或外部边界,让次要信息退到视觉后方。
3.2 网格与对齐:让图有隐藏在背后的秩序感
对齐这件事,技术人有时候会忽略,但设计师永远不会。为什么有些图看起来“很稳”,有些图看起来“不太舒服”?大部分时候都是对齐的问题。节点没有对齐、连线没有垂直水平、模块间距忽大忽小,都会让读者产生难以言说的不信任感。
我画图时会脑子中放一张看不见的网格:核心模块与核心模块之间保持相同间距,同层节点尽量放在同一水平线上,跨层连线尽量走正交线段而不是斜线。如果你用的是 draw.io,打开对齐辅助线再画,效果会好很多;如果是代码类工具,D2 的自动布局通常默认采用分层布局,本来就是对齐的。还有一个小技巧:相邻组之间保持一致的留白,比如所有模块之间的横向间距统一为 40px,纵向间距统一为 60px。这种秩序感不需要用户刻意察觉,但一定会影响阅读体验。
3.3 阅读动线:让读者按照你规划的路径走
读图和读文章一样,有一个视线移动的顺序。传统网页是从左到右、从上到下,系统架构图则多为自上而下的数据流,这也是大多数人默认的“主链路”。所以,画一张用户从发起请求到收到响应的时序图,我会把时间方向固定为从上到下;画一张系统分层架构图,我会把用户入口放最上方,数据存储放最下方,中间按逻辑层展开。
阅读动线设计的核心是:主链路上的元素必须处于视觉中心,支线细节往两侧或下方放。这不是拍脑门的规矩,它符合人的自然视觉习惯。如果一张图的主链路东一个节点西一个节点,读者的视线会在图里往返跳跃,信息读取效率会断崖式下降。我见过最好的做法,是把主链路设计成一个近似直线或“Z”字型的路径,让读者能一口气读完主干,再回头研究分支。
3.4 颜色与文本规范:有限色板能救整张图
颜色的作用不是“好看”,而是区分语义。最典型的做法是定义一套固定色板:核心业务模块用一种色系,外部依赖用另一种色系,异常或告警路径用红色,稳定已上线路径用绿色。整张图里的颜色数量控制在 4 到 6 种以内,超出这个数量,色块就会变成干扰。
除了颜色,文本同样需要做控制。节点名称尽量不超过两行,能用一个词说清楚就不要用一句话;箭头上的标签,优先用名词或动词短语,比如“HTTP”“异步”“创建订单”,而不是一整行描述性文字。字号上,主标题最大,分组标签次之,节点内文字再次之,连线上标签最小。我自己的默认设定是:主标题 16px,分组标签 13px,节点文字 12px,连线标签 10px,这样整张图的文字秩序是清晰的。
3.5 实操示例:画一张注册模块的局部架构图
用一个例子把前面几个原则串起来。假设要画“用户注册”的局部架构图,主链路是:客户端提交表单 -> 网关转发 -> 用户服务校验并入库 -> 消息队列发送欢迎通知。
画之前我先确定阅读动线:从上到下。客户端在最顶部,网关在第二层,用户服务在第三层,数据库和消息队列在第四层,搜索服务和邮件服务兜底在底部。主链路上的节点,我都用相同大小的矩形,边框颜色统一为主色;数据库用圆柱体区分形状,消息队列用柱状图,外部依赖(比如邮件服务)用浅灰背景并加上“外部”角标。页面左边放一个输入参数说明的分组框,右边放异常处理的跳转分支,它们都不是主线,所以用浅色虚线框包起来,弱化存在感。
这样即使读者不看任何文字解释,也能理出“发起注册 -> 校验入库 -> 异步通知”的主脉络,后续有问题再从两侧分组框去找细节。这就是一套完整的层级、网格、动线、颜色设计下来形成的效果。
4. 把图表变成工程资产:diagram as code 的自动化实战
4.1 图为什么要进入版本控制
很多团队的项目文档里,图片是一张 PNG,或者是 draw.io 的源文件,但源文件不会随代码评审走,最后图片和代码就越来越不一致。把图纳入版本控制,最大的价值不是“好看”,而是让图和代码一样具有历史、可评审、可回溯。
用代码描述图之后,一次改动可能只是某个节点名称变了,Pull Request 里能清楚看到变化的是哪几行;如果直接截图更新 PNG,评审者只能看到新旧两张图,肉眼比对差异。另一个好处是,图可以跟着文档仓库走,服务重构后更新依赖描述文件,重新渲染一遍,所有引用位置都能同步更新。图不再是“做完就丢的一次性产物”,而是一份持续演进的技术资料。
4.2 一个可复用的“文档内嵌图”方案
我目前用得比较顺手的方案是:Markdown 文档 + D2 代码块 + 构建时渲染。D2 相比其他同类工具有几个吸引我的点:语法直观(类似声明式)、布局引擎稳定、支持集群和标签,并且渲染出的 SVG 可以设置主题,代码风格上更接近现代 DSL 而不是上世纪语法。
具体工作流是:在 docs/diagrams 目录下放 .d2 源文件,在 Markdown 中引用它对应的 SVG 图片,然后在本地或 CI 里用一条命令把 .d2 渲染成 SVG。每次改图,我只需要编辑源文件、提交、重新渲染,文档里的图就自动更新了。这套组合不挑文档系统,MkDocs、VitePress、GitBook 都能用。如果你更熟悉 PlantUML,也可以套用同样的思路,只是语法习惯不同。
4.3 D2 实战:一段简单的微服务架构图
下面用 D2 写一个非常简单的微服务链路,大家感受一下这套“代码画图”的方式。
用户: 用户端 网关: API 网关 订单: 订单服务 库存: 库存服务 支付: 支付服务 用户 -> 网关: HTTPS 网关 -> 订单: 创建订单 订单 -> 库存: 扣库存 订单 -> 支付: 发起支付这段代码定义了四个节点和四条关系线,用箭头上的文本来标注协议或行为。D2 会自动计算布局,输出一张自上而下的依赖图。如果想让订单服务和库存、支付之间形成更清晰的分组,可以引入 cluster:
订单: 订单服务 { 创建订单 查询订单 } 库内: 依赖服务 { 库存 支付 } 订单.创建订单 -> 库内.库存: RPC 订单.创建订单 -> 库内.支付: RPC这种写法把“订单服务内部的子模块”和“它依赖的外部服务”明确区分开,渲染出来以后,读者能一眼看出哪些模块属于同一个服务边界。D2 里的 cluster 非常像真实架构中的“服务边界”或“命名空间”,这个表达能力和叙事价值是普通拖拽画布比较难做到的。
4.4 让图在文档系统里自动更新的集成细节
有了源文件和图片输出,只剩最后一步:自动化。我在 MkDocs 项目里通常加一个构建步骤,先执行 d2 渲染所有 .d2 文件到 images 目录,再执行 mkdocs build。在 CI 流水线上也会挂一个对应的 job,确保每次代码合并前,图都被重新生成,而不是有人忘了更新 PNG。
这里有几个容易踩的细节:第一,不同操作系统字体不一致会导致 SVG 排版差异,建议 CI 里固定使用同一种字体基础镜像;第二,生成的文件名尽量用语义化命名,比如 architecture-order-detail.svg,不要叫 final_v3.svg;第三,如果文档站点需要深色主题,可以考虑让 D2 同时输出一套浅色和深色主题的 SVG,用 CSS 的 prefers-color-scheme 切换,而不是强迫所有页面统一用亮色或暗色风格。这些看起来是小事,但实际维护体验差距很大。
5. 常见问题与排查技巧实录
5.1 布局乱跳:为什么同一个源文件换台机器图就变了
代码类图工具最令人头疼的问题之一,就是同一个 .d2 文件在本机渲染和 CI 上渲染出来的图不完全一样。多数原因不是代码写错,而是字体缺字导致节点宽度变化,或者是自动布局引擎版本差异。排查思路是固定渲染环境:用同一版本的工具、同一套字体配置、同一个基础镜像。如果图确实有大范围差异,再去检查定义里是否有依赖外部动态数据的变量。
我的习惯是在项目根目录放一个 .tool-versions 或 requirements.txt,把 D2 或 Graphviz 的版本锁住。这看起来是工程化手段,但往往能解决百分之八十的“玄学布局问题”。
5.2 中文乱码与字体缺失:永远不要忽略字体配置
用代码类工具画图,中文乱码是一个高频问题。多数情况下是因为当前环境没有安装支持中文的字体,渲染出来的 SVG 里中文变成方框或者完全不显示。解决方案很简单,但也很容易被忽略:在 Dockerfile 或 CI 构建环境里明确安装中文字体,比如 Noto Sans CJK SC。如果是本地渲染,把字体文件放到项目中,通过 D2 或 Graphviz 的配置文件指定字体名,就能确保不同机器上效果一致。
另外提一句,中英文混排时,建议把默认字体设置为包含中文子集的英文字体,比如 Inter 加上 Noto Sans CJK SC 的 fallback,这样优先显示英文字形,中文也有兜底。别小看字体这一步,很多图“突然变丑”都是字体回退导致的。
5.3 图太大渲染卡顿:复杂度需要主动拆分
画到一定规模,工具再快也会开始卡顿。我见过有人在一个画布里画两百多个节点,最后打开要等好几秒,导出的 SVG 十几兆,放在文档里浏览器都扛不住。这种问题靠优化渲染引擎解决不了,只能靠图的结构去治:把一张巨图拆成一张总览图加若干张局部详图,总览图只展示服务边界和数据主干,局部详图负责细节。
拆图的另一个好处是每张图都有自己的叙事焦点。总览图讲清楚系统有哪些服务、边界在哪、数据从哪里进从哪里出;局部图讲清楚某个服务内部流程。这样文档的层次也变清晰了,读者可以按需查阅,而不是对着大图来回拖动寻找目标。我的经验是:单张图节点数尽量控制在 30 到 50 个以内,如果超过这个量级,说明架构本身需要分层表达。
5.4 多人协作时风格漂移:建立团队图表规范
画图的风格问题,跟写代码的风格问题一样,如果不定义规范,每个人都会按自己的习惯来。团队里一旦出现“五颜六色的箭头”“节点一会儿圆角一会儿直角”,图的可信度就会下降。我通常会建议团队维护一份简短的图表规范,内容不需要厚,几页就够:节点形状的语义、主色和辅助色的具体色值、箭头线型含义、字体字号、导出图片的尺寸和格式。
规范最好直接写进文档仓库里,并给出标准模板文件。新成员入职后,让他们基于模板画图,而不是从空白画布开始,能省掉大量后期统一成本。最后再分享一个我个人很受用的自检习惯:图保存之前,退后一步问自己——如果一个从没见过这套系统的人,只花三十秒看这张图,能不能说出主链路是什么?如果说不出来,我会重新调整层级和视觉动线,而不是急着加更多文字解释。先让图自己能说话,再去补充细节,这就是我在 diagram-design 这件事上最大的心得。