1. 从“diagram-design”这个标题说起:它到底在解决什么问题
第一次看到“diagram-design”这个标题,很多人会以为它只是一个画图工具的教程。但如果你真正做过技术方案、写过设计文档、或者给团队做过架构评审,就会明白:画图从来不是目的,把复杂系统讲清楚才是。diagram-design 这个主题,本质上是在解决一个非常具体的痛点——如何用一套可复用、可维护、可协作的方式,把脑子里那些纠缠不清的模块、数据流、调用链路,变成一张别人三秒钟就能看懂的图。
我见过太多团队在画图上踩坑。有人用某款在线工具画了二十张架构图,结果版本一多,自己都分不清哪张是最新的;有人用代码生成图,结果改一个节点位置要重新跑一遍脚本;还有人干脆用 PPT 手绘,最后图是画完了,但没人愿意改,因为改一张图比写一千字文档还累。diagram-design 要解决的,就是这些看起来琐碎、实际上极其消耗团队效率的问题。
这篇文章适合三类人看:第一类是需要频繁输出技术图表的开发者或架构师,第二类是需要维护文档体系的技术写作者,第三类是希望把“画图”这件事从个人手艺变成团队规范的技术负责人。我会从工具选型、设计原则、实操流程、协作维护四个维度,把 diagram-design 这件事拆开揉碎讲清楚。你不需要有设计背景,也不需要会什么高级绘图软件,只要你会写代码、会思考结构,就能跟着做出一套真正能用的图表体系。
在开始之前,先明确一个核心观点:diagram-design 的关键不在于“画得好看”,而在于“画得对、改得动、传得开”。好看是加分项,但结构清晰、语义准确、维护成本低才是及格线。下面我会围绕这个观点,逐步展开。
2. 工具选型:为什么我最终放弃了纯拖拽式绘图
2.1 拖拽式工具的隐性成本
很多人一开始都会用拖拽式工具,因为上手快、所见即所得。我早期也用过,画几张简单的流程图确实没问题。但一旦图的数量超过十张,或者需要多人协作,问题就暴露了。
首先是对齐和间距。拖拽式工具里,你很难保证多个节点之间的间距完全一致。手动对齐十次,可能有八次都是歪的。其次是版本管理。大部分拖拽式工具的文件格式是私有的,你没法用 Git 做 diff,改了什么全靠记忆。最后是复用性。你画了一个标准的数据流图,想在新项目里复用,只能复制粘贴,然后手动改每个节点的文字,改到第三张就想砸键盘。
这些问题的根源在于:拖拽式工具把“图”当成了一张静态图片,而不是一份结构化数据。而 diagram-design 的核心思路,是把图当成代码来管理。
2.2 代码化绘图工具的核心优势
代码化绘图,简单说就是用文本描述图的结构,然后由渲染引擎生成图片。常见的方案有 Mermaid、PlantUML、Graphviz、D2 等。它们共同的优点是:
- 版本可控:纯文本文件,Git 可以追踪每一行改动。
- 批量修改:改一个全局样式,所有图同步生效。
- 复用性强:可以把常用节点、子图抽成模板,跨项目引用。
- 协作友好:多人编辑同一个文本文件,冲突解决比二进制文件容易得多。
我最终选择的是D2作为主力工具,配合Mermaid做快速草图。D2 的语法比 PlantUML 更现代,布局算法也更智能,尤其是它对容器、嵌套、跨层连接的支持,非常适合画架构图。Mermaid 则胜在生态广,很多文档平台原生支持,适合写 README 或轻量级说明。
下面是一个 D2 的简单示例,画一个三层架构:
用户: { 浏览器 移动端 } 网关: { API Gateway 负载均衡 } 服务层: { 用户服务 订单服务 支付服务 } 数据库: { 用户库 订单库 支付库 } 用户.浏览器 -> 网关.API Gateway: HTTPS 用户.移动端 -> 网关.API Gateway: HTTPS 网关.API Gateway -> 服务层.用户服务: gRPC 网关.API Gateway -> 服务层.订单服务: gRPC 网关.API Gateway -> 服务层.支付服务: gRPC 服务层.用户服务 -> 数据库.用户库: SQL 服务层.订单服务 -> 数据库.订单库: SQL 服务层.支付服务 -> 数据库.支付库: SQL这段代码渲染出来的图,节点自动对齐,连线自动避让,改一个服务名只需要改一行文本。这就是代码化绘图的核心价值:把精力从“调位置”转移到“理逻辑”。
2.3 工具选型的决策表
为了让你更直观地做选择,我整理了一个对比表:
| 工具 | 语法友好度 | 布局能力 | 生态支持 | 学习曲线 | 适合场景 |
|---|---|---|---|---|---|
| Mermaid | 高 | 中 | 极广 | 低 | README、轻量流程图 |
| PlantUML | 中 | 中 | 广 | 中 | UML、时序图 |
| Graphviz | 低 | 高 | 中 | 高 | 复杂关系图 |
| D2 | 高 | 高 | 中 | 低 | 架构图、系统图 |
| Excalidraw | 高 | 低 | 广 | 极低 | 手绘风格草图 |
我的建议是:主力用 D2,辅助用 Mermaid,草图用 Excalidraw。D2 负责正式架构图,Mermaid 负责嵌入文档的轻量图,Excalidraw 负责早期讨论时的快速表达。三者配合,基本覆盖了从草稿到定稿的全流程。
注意:不要试图用一个工具解决所有问题。我见过有人非要用 Mermaid 画复杂架构图,结果布局乱成一团,最后还是要手动调。工具是有边界的,承认边界才能选对工具。
3. 设计原则:一张好图应该具备的四个特征
3.1 语义准确:每个节点和连线都有明确含义
画图最怕的是“看起来对,实际上错”。比如你把一个消息队列画成了数据库,把异步调用画成了同步调用,这种错误在评审时会被一眼看穿,但如果你自己画的时候没想清楚,就会埋下隐患。
我的做法是:在画图之前,先用文字把系统描述一遍。比如“用户请求先经过网关鉴权,然后转发到订单服务,订单服务写入数据库后发送消息到队列,由库存服务异步消费”。这段文字里的每个动词、每个名词,都对应图上的一个元素。文字写清楚了,图就不会画错。
具体来说,节点代表实体(服务、数据库、队列、外部系统),连线代表关系(调用、依赖、数据流),连线的方向代表流向,连线的标签代表协议或动作。这四要素缺一不可。
3.2 层次分明:用容器和分组表达边界
一张图如果所有节点都平铺在一起,读者很难理解哪些属于同一个系统、哪些属于同一个团队。D2 和 Mermaid 都支持容器语法,可以把相关节点包在一个框里。
比如:
前端: { Web iOS Android } 后端: { 网关 用户服务 订单服务 } 基础设施: { 数据库 缓存 消息队列 } 前端.Web -> 后端.网关 后端.用户服务 -> 基础设施.数据库 后端.订单服务 -> 基础设施.消息队列容器不仅让图更整洁,更重要的是它表达了边界。边界是架构图的核心信息之一:哪些东西在同一个部署单元里,哪些东西跨了网络,哪些东西属于同一个团队维护。这些信息比节点本身更重要。
3.3 一致性:同一套图里用同一套视觉语言
我见过很多团队的图,第一张用蓝色表示数据库,第二张用绿色表示数据库,第三张干脆用图标代替。读者每看一张图都要重新学习图例,认知负担极大。
diagram-design 的一个重要原则是:建立并遵守一套视觉规范。比如:
- 所有数据库统一用圆柱体图标
- 所有外部系统统一用虚线框
- 所有异步调用统一用虚线箭头
- 所有同步调用统一用实线箭头
- 所有服务统一用圆角矩形
这套规范一旦定下来,就不要轻易改。D2 支持自定义样式和主题,你可以把规范写成一个样式文件,所有图引用同一个文件。这样即使换了项目,视觉语言也是一致的。
3.4 可维护:改一处不影响全局
这是代码化绘图最大的优势,但前提是你得用对。很多人写 D2 或 Mermaid 时,喜欢把样式内联在每个节点上,比如:
用户服务: { style.fill: "#e1f5fe" style.stroke: "#0288d1" }这样写一次两次没问题,但节点一多,改一个颜色要改几十处。正确的做法是定义样式类:
classes: { 服务: { style.fill: "#e1f5fe" style.stroke: "#0288d1" } 数据库: { shape: cylinder style.fill: "#fff3e0" } } 用户服务: { class: 服务 } 订单服务: { class: 服务 } 用户库: { class: 数据库 }这样改样式只需要改类定义,所有引用该类的节点自动更新。这就是“改一处不影响全局”的正确姿势。
提示:样式类最好按语义命名,比如“服务”“数据库”“队列”“外部系统”,而不是按颜色命名。按颜色命名的话,哪天你想把蓝色改成绿色,类名就变得毫无意义了。
4. 实操流程:从空白文件到可交付图表的完整步骤
4.1 第一步:明确图的受众和目的
在动手写代码之前,先问自己三个问题:
- 这张图给谁看?是给开发同事看,还是给产品经理看,还是给外部合作方看?受众不同,详细程度和术语使用完全不同。
- 这张图要回答什么问题?是展示系统整体架构,还是展示某个流程的时序,还是展示数据模型?目的不同,图的类型不同。
- 这张图会放在哪里?是放在设计文档里,还是放在演示文稿里,还是放在代码仓库的 README 里?场景不同,尺寸和复杂度不同。
我自己的习惯是:给开发看的图可以复杂,给非技术看的图必须简化。给开发看的图可以出现“gRPC”“Kafka”“Redis”这些术语,给产品看的图最好用“服务”“消息”“缓存”这种通用词。
4.2 第二步:用文字列出所有节点和关系
不要一上来就写代码。先用一个简单的文本列表,把所有节点和关系列出来。比如:
节点: - 用户(外部) - API 网关 - 用户服务 - 订单服务 - 用户数据库 - 订单数据库 - 消息队列 关系: - 用户 -> API 网关:HTTPS - API 网关 -> 用户服务:gRPC - API 网关 -> 订单服务:gRPC - 用户服务 -> 用户数据库:SQL - 订单服务 -> 订单数据库:SQL - 订单服务 -> 消息队列:异步消息这个列表就是图的“骨架”。骨架对了,图就不会跑偏。我见过很多人跳过这一步,直接写代码,结果画到一半发现漏了一个关键服务,又回头改,效率反而更低。
4.3 第三步:选择合适的图类型
不同的目的对应不同的图类型:
| 目的 | 推荐图类型 | 工具 |
|---|---|---|
| 展示系统整体结构 | 架构图 | D2 |
| 展示请求处理流程 | 时序图 | Mermaid |
| 展示数据模型 | ER 图 | Mermaid / D2 |
| 展示状态流转 | 状态图 | Mermaid |
| 展示部署拓扑 | 部署图 | D2 |
| 展示业务流程 | 流程图 | Mermaid |
选对图类型,读者理解起来会轻松很多。比如你想展示“用户下单后发生了什么”,用时序图就比架构图直观得多,因为时序图天然表达了先后顺序。
4.4 第四步:编写代码并迭代布局
以 D2 为例,写完之后用d2命令渲染成 SVG 或 PNG:
d2 --theme 0 --layout dagre architecture.d2 architecture.svg如果布局不理想,可以调整布局引擎。D2 支持dagre、elk、tala等多种布局算法。dagre适合大多数场景,elk适合复杂嵌套,tala适合精细控制。我的经验是:先用默认布局,不满意再换引擎,最后才手动调位置。手动调位置是最后手段,因为一旦手动调了,后续改节点就要重新调,维护成本很高。
4.5 第五步:导出与嵌入
D2 支持导出 SVG、PNG、PDF 等格式。SVG 是最推荐的,因为它是矢量图,放大不模糊,而且可以用 CSS 控制样式。嵌入文档时,直接用 Markdown 的图片语法:
如果文档平台支持 Mermaid,也可以直接嵌入 Mermaid 代码块,这样图会随文档一起渲染,不需要额外维护图片文件。
注意:如果你的图会频繁改动,建议用 SVG 并提交到代码仓库,和代码一起做版本管理。如果图比较稳定,导出 PNG 嵌入文档也可以,但记得保留源文件,否则以后想改就麻烦了。
5. 协作与维护:让图表体系活得更久
5.1 目录结构:把图当成代码来组织
我建议在项目根目录下建一个diagrams文件夹,结构如下:
diagrams/ ├── styles/ │ └── theme.d2 ├── architecture/ │ ├── overview.d2 │ └── user-service.d2 ├── flows/ │ ├── order-flow.d2 │ └── payment-flow.d2 └── README.mdstyles/theme.d2存放全局样式类,所有图通过...theme.d2导入。architecture/存放架构图,flows/存放流程图。README.md说明每张图的用途和更新记录。
这样的结构清晰、可扩展,新人接手时也能快速找到需要的图。
5.2 命名规范:让文件名自己说话
文件名不要用diagram1.d2、diagram2.d2这种无意义的命名。推荐用“主题-类型”的格式:
overview-architecture.d2order-sequence.d2user-data-model.d2deployment-topology.d2
这样即使不看内容,光看文件名就知道这张图是干什么的。搜索的时候也方便,比如搜sequence就能找到所有时序图。
5.3 更新机制:图与代码同步演进
图表最大的敌人是“过时”。代码改了,图没改,图就变成了误导。我的做法是:把图的更新纳入代码评审流程。每次有架构变更的 PR,必须同时更新对应的图。如果 PR 里只改了代码没改图,评审时直接打回。
具体操作上,可以在 CI 里加一个检查:如果diagrams/目录下的文件有改动,自动渲染并生成预览图,附在 PR 评论里。这样评审的人能直接看到图的变化,不用自己跑命令。
5.4 常见协作问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 多人同时改一张图冲突 | 文本文件冲突 | 拆分图,按模块分文件 |
| 图与代码不一致 | 缺少同步机制 | 纳入 PR 评审,CI 检查 |
| 新人看不懂图例 | 缺少说明 | 在 README 里维护图例说明 |
| 图太复杂没人看 | 信息过载 | 拆成多张图,每张只讲一件事 |
| 样式不统一 | 没有全局样式 | 抽取样式类,统一引用 |
这些问题的根源,其实都是“把图当成一次性产物”而不是“长期资产”。一旦你开始用管理代码的方式管理图,这些问题都会迎刃而解。
6. 我踩过的坑和总结出的几条硬经验
6.1 不要追求一张图讲完所有事
我早期特别喜欢画“大而全”的架构图,恨不得把所有服务、所有数据库、所有中间件都塞进一张图。结果就是:图巨大无比,打印出来看不清,屏幕上要放大三倍才能读。后来我学乖了,一张图只讲一件事。整体架构一张,用户服务内部一张,订单流程一张,部署拓扑一张。每张图都有自己的主题,读者按需查看。
6.2 颜色不要超过五种
颜色是用来表达语义的,不是用来装饰的。我见过有人用十几种颜色画图,结果读者根本记不住哪种颜色代表什么。我的建议是:主色不超过三种,辅助色不超过两种。比如蓝色表示服务,绿色表示数据库,橙色表示外部系统,灰色表示基础设施。足够了。
6.3 连线尽量少交叉
连线交叉是图可读性的大敌。D2 和 Graphviz 都有自动避让算法,但如果你节点顺序排得不好,交叉还是很多。我的经验是:把调用关系密集的节点放在一起,把跨层的连线尽量拉直。如果一张图的连线交叉超过五处,就考虑拆图或者调整布局。
6.4 给图加标题和图例
很多人画图不加标题,读者看了半天不知道这张图在讲什么。标题要写在图的上方,用一句话概括图的主题。图例要放在图的角落,说明每种形状、每种颜色、每种线型的含义。这两样东西花不了五分钟,但能极大提升图的可读性。
6.5 定期清理过时的图
图也会“腐烂”。过时的图比没有图更糟糕,因为它会误导人。我每季度会花半小时清理diagrams/目录,把不再使用的图删掉,把过时的图更新或标记为“已废弃”。保持图表体系的整洁,和保持代码整洁一样重要。
6.6 不要忽略移动端阅读
现在很多人用手机看文档,如果你的图在手机上完全看不清,那它的价值就大打折扣。我的做法是:关键图导出两个版本,一个宽屏版,一个移动版。移动版减少节点、简化连线,保证在小屏幕上也能看清主干逻辑。
7. 一个完整的实战案例:从零画一张订单系统架构图
7.1 需求描述
假设我们要为一个订单系统画架构图,系统包含以下组件:
- 客户端:Web、iOS、Android
- 网关层:API Gateway
- 服务层:用户服务、订单服务、支付服务、库存服务
- 数据层:用户库、订单库、支付库、库存库
- 中间件:消息队列、缓存
调用关系:
- 客户端通过 HTTPS 调用网关
- 网关通过 gRPC 调用各服务
- 订单服务写入订单库后,发送消息到队列
- 库存服务消费队列消息,更新库存库
- 各服务读取缓存加速查询
7.2 编写 D2 代码
...styles/theme.d2 客户端: { Web iOS Android } 网关层: { API Gateway } 服务层: { 用户服务 订单服务 支付服务 库存服务 } 数据层: { 用户库: { class: 数据库 } 订单库: { class: 数据库 } 支付库: { class: 数据库 } 库存库: { class: 数据库 } } 中间件: { 消息队列: { class: 队列 } 缓存: { class: 缓存 } } 客户端.Web -> 网关层.API Gateway: HTTPS 客户端.iOS -> 网关层.API Gateway: HTTPS 客户端.Android -> 网关层.API Gateway: HTTPS 网关层.API Gateway -> 服务层.用户服务: gRPC 网关层.API Gateway -> 服务层.订单服务: gRPC 网关层.API Gateway -> 服务层.支付服务: gRPC 网关层.API Gateway -> 服务层.库存服务: gRPC 服务层.用户服务 -> 数据层.用户库: SQL 服务层.订单服务 -> 数据层.订单库: SQL 服务层.支付服务 -> 数据层.支付库: SQL 服务层.库存服务 -> 数据层.库存库: SQL 服务层.订单服务 -> 中间件.消息队列: 异步消息 中间件.消息队列 -> 服务层.库存服务: 消费 服务层.用户服务 -> 中间件.缓存: 读写 服务层.订单服务 -> 中间件.缓存: 读写7.3 样式文件
classes: { 服务: { style.fill: "#e3f2fd" style.stroke: "#1565c0" style.border-radius: 8 } 数据库: { shape: cylinder style.fill: "#fff3e0" style.stroke: "#ef6c00" } 队列: { shape: queue style.fill: "#f3e5f5" style.stroke: "#7b1fa2" } 缓存: { shape: stored_data style.fill: "#e8f5e9" style.stroke: "#2e7d32" } }7.4 渲染与检查
渲染命令:
d2 --theme 0 --layout elk order-architecture.d2 order-architecture.svg渲染完成后,检查以下几点:
- 所有节点是否都在正确的位置
- 连线是否清晰,有没有不必要的交叉
- 颜色是否一致,图例是否完整
- 文字是否清晰,有没有被截断
- 整体布局是否平衡,有没有一边倒
如果发现问题,调整代码后重新渲染。通常迭代两到三次就能得到满意的结果。
7.5 嵌入文档
最后,把生成的 SVG 嵌入到设计文档中:
## 订单系统架构  如上图所示,订单系统采用分层架构,客户端通过网关访问后端服务, 订单服务在写入数据库后通过消息队列异步通知库存服务。这样,一张完整的架构图就交付了。整个过程从需求描述到最终嵌入,熟练之后大约需要二十分钟。相比拖拽式工具,前期学习成本略高,但后续的维护成本极低。
8. 关于 diagram-design 的一些个人体会
做了这么多年的图表设计,我最大的体会是:画图是一种思维方式,工具只是载体。你脑子里如果对系统结构不清楚,用什么工具都画不出好图;你脑子里如果清楚,哪怕用最简单的工具也能画出有价值的图。
diagram-design 这个主题,表面上是讲怎么画图,实际上讲的是怎么把复杂系统拆解成可理解、可沟通、可维护的结构。这个过程需要你对系统有深入的理解,需要你站在读者的角度思考,需要你克制“把所有东西都塞进去”的冲动。
我现在的习惯是:每做一个新系统,先画图,再写代码。图是设计的草稿,也是沟通的媒介。图改清楚了,代码写起来就顺了。反过来,代码写完了再补图,往往只是“记录”,而不是“设计”。
如果你刚开始接触代码化绘图,我的建议是从小图开始。不要一上来就画整个系统,先画一个模块,画一个流程,画一个数据模型。画上十张之后,你自然就有感觉了。工具的命令和语法可以查文档,但“怎么把一件事讲清楚”这种能力,只能靠多画多练。
最后分享一个我常用的检查方法:画完图之后,把图给一个不了解这个系统的同事看,让他用自己的话复述一遍图的内容。如果他复述得准确,说明图合格了;如果他复述得磕磕绊绊,说明图还有歧义。这个方法比任何评审都有效,因为它是从读者视角出发的终极检验。