你有没有经历过这种场景:技术方案评审前夜,架构图画到凌晨两点;文档更新了三次,图却还停留在一个月前的老版本;同一个模块在两张图里画出了完全不同的依赖关系。我之前深受其扰,直到把 diagram-design 当成写代码来搞,这些问题才算真正解决。
所谓 diagram-design,简单说就是“用代码定义图表”,最主流的落地方式是 Markdown 内嵌 Mermaid 语法,写graph TD就生成流程图,写sequenceDiagram就生成时序图,改图画图全在文本里完成。这项能力非常适合写技术文档、画系统架构、梳理业务流程的开发者、架构师、运维和技术博主。它解决了三件事:图随代码走、变更可追踪、内容可复用,也是我近两年最推荐的图表设计模式之一。
这篇博文我会从设计思路、工具选型、语法细节、完整实操到踩坑经历,一次讲透我实际使用 diagram-design 的完整方法论,内容偏实战,建议找个编辑器边看边敲。
1. 为什么我最终选定了“图即代码”这条路
1.1 从两张图的暗战说起——手绘与代码绘图的边界
先讲一个我自己的项目经历。去年维护一个内部中间件,文档里有一张整体架构图,是从同事的旧文档里继承来的。当时项目已经从单体拆成了四个微服务,数据库也换了类型,但那张图还是三年前的单体结构。我花了半小时用画图工具改了两版,结果代码合并时文档冲突,图直接从最新版回退到了旧版。后来换成 Mermaid 写架构图,每次服务变更只需要改几行文本,再也不用担心图和代码脱节。
手绘派和代码派的边界其实很明显。用 draw.io、Excalidraw 这类拖拽工具,优点是排版自由、上手快;缺点是一旦图复杂起来,元素的坐标调整非常痛苦。而且这类工具的源文件要么是 XML、要么是私有格式,放到 Git 里做 diff 基本等于摆设。而 diagram-design 把图的内容收敛为纯文本,每一处修改都能像代码一样走评审、留记录。对于需要多人维护的技术文档,这种可追溯性就是最大的优势。
我并不是说拖拽工具一无是处,而是在“长期维护 + 多人协作 + 需要版本控制”这三个条件下,代码化绘图有压倒性优势。
1.2 source 即真相:用文本管理图表资产
我理解的 diagram-design 核心逻辑是:图表是一个派生产物,真正的数据源是那段文本描述。文本可以进 Git、可以 diff、可以 comment,每一个历史版本都带着当时的上下文,这才是“图即代码”最值钱的地方。
举个例子,有一次功能迭代要改调用链,我在 PR 描述里用 Mermaid 画了新旧两条链路,评审人直接在评论区对着语法提意见。这在拖拽工具里很难做到——你总不能把 draw.io 的 XML 贴进评论区让人看。但文本形式就可以。哪怕对方没有装任何绘图工具,也能读懂A-->B表达的含义。
这种模式还有一个隐藏好处:图的描述天然带有结构化信息,可以配合脚本做自动化分析。比如批量扫描全仓库的 Mermaid 代码,统计服务间依赖数量,或者检查有没有循环依赖。这是传统图片完全做不到的。
2. 工具选型:Mermaid、PlantUML、Graphviz 还是 Excalidraw
2.1 四款主流工具的横向对比
真正上手 diagram-design 前,我也在工具选型上纠结了很久,用过 PlantUML、Graphviz、Excalidraw,最近两年基本固定在 Mermaid。下面这张表是我实测过的结论,方便你根据场景对号入座:
| 工具 | 语法难度 | 图形类型 | 中文支持 | 生态集成 | 适合场景 |
|---|---|---|---|---|---|
| Mermaid | 低,半小时上手 | 流程图、时序图、状态图、甘特图、类图等 | 好,注意字体配置 | GitHub、GitLab、Notion、Typora 等 | 技术文档、PR 描述、架构图 |
| PlantUML | 中,类 Java 语法 | 时序图、用例图、活动图为主 | 一般,中文偶尔乱码 | Jenkins、Confluence 插件 | 偏 UML 的软件工程场景 |
| Graphviz | 高,需了解 dot 语言 | 通用有向/无向图,布局算法强 | 取决于渲染引擎 | 可嵌入各类脚本 | 复杂节点关系、自动化生成图 |
| Excalidraw | 低,纯鼠标 | 手绘风格自由图形 | 好 | 实时协作强 | 产品原型、头脑风暴、白板 |
如果你只是偶尔画一两张草图,Excalidraw 的体验确实很爽,手绘风格也更亲切。但只要是输出到稳定的技术文档,我首选 Mermaid,原因见下一节。
2.2 我为什么在大多数场景下选 Mermaid
选 Mermaid 有很多客观理由,比如它内嵌在 Markdown 生态里,GitHub、GitLab、Typora 原生支持,不需要额外插件;语法足够简洁,不用记太多关键字;还有官方 Live Editor,改完左边右边立即渲染,非常利于排错。
但真正让我彻底切换的是一次文档维护经历。以前用 PlantUML 画时序图,语法相对繁琐:每个参与对象要定义别名,还要手动标注激活条位置。换成 Mermaid 之后,只需写参与者和消息箭头,所有布局交给渲染器。对于三天两头要调整的流程文档,这种“只管内容、不管布局”的写法能省下大量时间。
Graphviz 虽然布局算法很强大,但学习成本确实偏高。你想画一个漂亮的依赖图,得先去理解 dot 语言里的 subgraph、rank、edge 约束。Mermaid 内部本身也会调用类似 dagre 的布局引擎,绝大多数场景下足够用了。真遇到需要精细控制节点位置的场景,Mermaid 也有direction、classDef这些手段,后面实操部分会讲。
3. diagram-design 核心细节与语法拆解
3.1 节点、连线的底层逻辑
Mermaid 的流程图语法,核心只有两个概念:节点和连线。节点是图的实体,可以是方框、圆形、菱形;连线表达实体之间的关系。理解了这一点,Mermaid 的大部分语法都能归到“声明节点 + 声明连线”这个框架里。
节点声明的基本格式是id[显示文本]。中括号是普通矩形,圆角矩形用(文本),菱形用{文本},圆形用((文本))。连线的表示方法有很多变体:-->是带箭头实线,---是无箭头实线,-.->是带箭头虚线,==>是粗实线箭头,--文本-->表示带标签的连线。
这里要特别提醒:很多初学者会漏掉节点 ID 的可复用性。A["用户"] --> B["网关"],可以简写成A["用户"] --> B,后面想继续扩展,只需要写B --> C["订单服务"],不需要重新定义B。这种声明方式对后续维护非常友好,改一处名称,所有引用它的连线都会同步更新,根本不需要手工去改一堆坐标。
3.2 三类高频图的实操示例
第一类我觉得最常用的是流程图。一个标准的用户登录判断流程,代码大概是这样的:
graph TD A["用户输入账号密码"] --> B{"账号是否存在?"} B -- 否 --> C["提示账号不存在"] B -- 是 --> D{"密码是否正确?"} D -- 否 --> E["提示密码错误"] D -- 是 --> F["登录成功,进入首页"]这里的TD表示方向 top-down,即从上往下;改成LR就是左右布局。版本管理时代,这种简洁表达完全不需要注释就能看懂。我常用的流程图画法基本都在这个范式内,遇到需要分组的场景,会加 subgraph 包裹,后面单独讲。
第二类是时序图。时序图特别适合描述接口调用链,比如下单接口跨模块的调用过程:
sequenceDiagram participant U as 用户 participant C as 客户端 participant S as 订单服务 participant D as 数据库 U->>C: 点击下单 C->>S: POST /api/order S->>S: 校验商品库存 S->>D: 查询库存 D-->>S: 返回库存数量 S-->>C: 返回下单结果 C-->>U: 展示结果页时序图里->>是实线箭头,-->>是虚线返回。头部用participant声明参与者,可以自定义别名。掌握这两个点,日常画接口交互足够了。对于排查线上故障、向新人讲解调用链,时序图的效果比纯文字高一个量级。
第三类是状态图,适合描述有限状态机。比如订单状态流转:
stateDiagram-v2 [*] --> 待支付 待支付 --> 已支付: 用户完成支付 待支付 --> 已取消: 支付超时 已支付 --> 配送中: 商家发货 配送中 --> 已完成: 用户确认收货 已完成 --> [*]状态图的价值在于把所有合法流转路径显式地画出来。我每次做状态相关的代码评审,都会先在文档里补一张这样的图,很多遗漏的异常分支一眼就能发现。
3.3 布局与样式:不止是“能看”而是“好看”
diagram-design 的另外一个优势是样式可控。默认渲染效果虽然不难看,但在正式的架构文档或者技术分享中,我还是建议做一下差异化处理:用不同颜色的节点区分服务类型,用虚线表达异步消息,用 subgraph 表达系统边界。
一个微服务架构图的典型写法:
graph LR subgraph Client["客户端层"] Web["Web端"] App["移动端"] end subgraph Gateway["网关层"] G["API网关"] end subgraph Service["服务层"] Order["订单服务"] Pay["支付服务"] User["用户服务"] end Web --> G App --> G G --> Order G --> Pay G --> Usersubgraph的语义是“把若干节点圈进同一个泳道”,表达上非常有价值。在架构图里,泳道边界往往代表了部署边界、团队归属或者信任边界,读者一眼就能看出请求从哪来、到哪去、哪些服务属于同一层。
如果想统一样式,可以用classDef定义样式分类,再用class把节点归入分类。比如把数据库节点统一定义为橙色:
classDef db fill:#f9d0c4,stroke:#333,stroke-width:2px; class OrderDB,PayDB db;我通常会在图里区分三类颜色:外部系统用灰色、核心服务用蓝色、数据存储用橙色。这样导出成图片后,PPT 和分享文章里都不用额外加文字说明,信息层级直接可见。
4. 架构图设计实操:从需求到一张可维护的图
4.1 第一步先问自己:这张图给谁看
画图之前最重要的不是选工具,而是想清楚这张图的使用场景。同样是微服务架构,给老板看和给新同事看完全是两种画法。给老板看,关注核心模块数量和部署边界,技术细节越少越好;给新同事看,需要标明每个服务的位置、上下游依赖、关键接口调用顺序。
我会在画图前的草稿阶段列出三个问题:看这张图的人需要做出什么判断?这张图放在哪类文档里?未来多久更新一次?这三个问题的答案决定了图的规模和细节层次。如果只是 PPT 汇报用,图越简洁越好,用不上复杂标注;如果是长期维护的架构说明,尽量把每个依赖关系都显式画出来,避免埋坑。
4.2 实战:用 Mermaid 设计一个可演化的订单架构图
下面用一个模拟场景完整走一遍实操流程:订单服务、支付服务、用户服务三个核心模块,外加一个消息队列异步解耦。要求是能看出服务间依赖、事件流向和存储边界。
第一步先列节点清单,把所有实体列出来:
- 客户端(Web、App)
- 网关层
- 订单服务
- 支付服务
- 用户服务
- MySQL(订单库)
- Redis(缓存)
- RabbitMQ(消息队列)
第二步明确关系,画出一版文本描述:
graph TB Client["客户端"] --> Gateway["API网关"] Gateway --> OrderSvc["订单服务"] Gateway --> UserSvc["用户服务"] OrderSvc --> PaySvc["支付服务"] OrderSvc --> OrderDB[("MySQL订单库")] OrderSvc --> Cache[("Redis缓存")] OrderSvc -->|"发送下单事件"| MQ["RabbitMQ"] PaySvc -->|"支付结果事件"| MQ MQ -->|"异步通知"| OrderSvc UserSvc --> Cache第三步是考虑可读性。上面的图把所有节点放在一个层级里,虽然信息完整,但看多了容易晕。优化方向是加 subgraph 分层,同时给核心链路加粗:
graph TB subgraph Client["客户端层"] Web["Web端"] App["App端"] end subgraph Gateway["接入层"] APIGW["API网关"] end subgraph Services["核心服务层"] Order["订单服务"] Pay["支付服务"] User["用户服务"] end subgraph Storage["存储与中间件"] MySQL[("MySQL")] Redis[("Redis")] MQ["RabbitMQ"] end Web --> APIGW App --> APIGW APIGW --> Order APIGW --> User Order ==> Pay Order ==> MySQL Order --> Redis Order -->|"下单事件"| MQ Pay -->|"支付结果"| MQ MQ -->|"异步通知"| Order第四步是关于可扩展性。以后要加一个优惠券服务,只需要在核心服务层加一行节点声明,再补两条连线即可。这种更新成本,在传统绘图软件里至少要调整半小时,而在 diagram-design 里只需要 30 秒。
4.3 导出与集成:把图画进博客、Wiki 和 PPT
Mermaid 文本写完后,真实交付往往还需要图片文件。我常用的导出路径有三条。第一条是 Typora 之类支持 Mermaid 的 Markdown 编辑器,文档预览时右键保存 PNG。第二条是使用官方 Live Editor 在线编辑并导出 SVG 或 PNG。第三条是利用命令行工具mermaid-cli,用一条命令批量渲染。
命令行方式比较适合自动化场景,比如文档生成脚本。基本流程是先安装:
npm install -g @mermaid-js/mermaid-cli然后指定输入和输出:
mmdc -i architecture.mmd -o architecture.png -b white-b white指定背景色为白色,避免透明背景插到 PPT 里显得脏。我还有个习惯是导出 SVG 而不是 PNG,SVG 是矢量格式,放大多少倍都不糊,放到博客配图或分享文档里观感更好。
如果你用的是 GitHub 仓库,直接在 Markdown 里写 Mermaid 代码块就能渲染,省掉导出这一步,浏览体验也很流畅。这种情况下我通常不导出图片,只保留源文件,因为渲染是实时的,文档更新即图表更新。
5. 常见问题与排查技巧实录
5.1 “图怎么不按照我想要的顺序排列”
使用 flowchart 时经常遇到一个问题:明明希望两个节点左右排列,渲染出来却是上下排列。这不是 bug,而是 Mermaid 的布局引擎会自动优化节点位置,尽量减少连线交叉。解决办法有几个:
- 把
graph TD改成graph LR,让整体布局更倾向于左右方向。 - 利用不可见连线(
~~~)强制定位。比如想让B固定在A右边,可以写A ~~~ B,渲染时 B 会被拉到 A 旁边。 - 调整声明顺序。Mermaid 的布局受节点首次出现顺序影响,尽量按照“从左到右、从上到下”的阅读顺序声明节点。
这个方法对于分支较多的流程也很有效。比如某条分支有 4 个节点,另一条分支只有 1 个节点,引擎往往会拉伸短分支。这时可以在短分支末尾加一个空节点,再让空节点与主流程相连,布局会平衡很多。
5.2 中文显示乱码或方块
Mermaid 多数渲染器的中文支持已经不错,但当你用命令行导出图片时,如果环境缺少中文字体,渲染出来就是一片方块。解决方案是安装中文字体,比如 Linux 环境安装 fonts-noto-cjk:
apt-get install fonts-noto-cjkWindows 环境一般没这个问题,macOS 也内置了 PingFang。如果是 puppeteer 渲染时字体没识别到,还可以在启动参数里指定字体目录。实在搞不定,就把中文标签尽量改为英文,图片说明写在正文里。
5.3 复杂图渲染卡顿或超时
当节点数量超过 50 个时,Mermaid 的自动布局计算量会明显上升,甚至在 CI 环境里直接超时。我的经验是:复杂图拆成多张子图,而不是硬塞一张大图。一张图只表达一件事,这也是 diagram-design 的一个好习惯。
如果一定要展示全貌,可以使用subgraph先合并子模块,在总图中把每个子图当成一个整体。这样既保持了全局视野,又把布局复杂度降了下来。另外,mermaid-cli 有一个--maxTextSize参数可以调整文本量限制,但治标不治本,我建议优先从拆图角度解决。
5.4 多人协作时的版本冲突
这个问题在团队推广 diagram-design 时经常被低估。因为是文本文件,多人同时编辑容易出现冲突。我的实用方案是:一张图对应一个独立文件,不要和其他文档混在一起;提交时遵循“一次改动对应一个逻辑变更”的原则,避免大范围重排。这样即使冲突,Git 也能自动合并且冲突范围很小。
我在团队里推过一个约定:所有架构图源文件放在docs/diagrams/目录,文件名即图名,比如order-flow.mmd。文档里用相对路径引用图片输出,或者直接在文档内嵌 Mermaid 代码块。这个约定执行半年后,文档里再也没有出现过一张过期架构图,因为每次代码变更,相关图几乎同步更新成了同一批改动的一部分。
6. 最后再分享一点个人习惯
画 diagram-design 图这几年,我最大的改变是:愿意画图了。以前画图最怕“画错了要重排”,现在写图最怕“没想清楚关系就动手”。文本化以后,改图成本极低,反而倒逼我更多地去整理思路、梳理依赖。
我现在写技术方案的基本流程是:先列出所有实体,再用箭头标出关键关系,最后才考虑样式和层级。这个过程不需要打开画图软件,拿任意一个 Markdown 编辑器就能完成,甚至在手机上也可以。
如果你之前一直用拖拽工具,不妨从今天开始,把下一张架构图用 Mermaid 写出来。先接受它默认的排版,等流程跑顺了,再慢慢加 subgraph、样式和布局优化。用不了几次,你就会发现 diagram-design 真正帮你省掉的不是画图的时间,而是后续维护和协作中那些看不见的沟通成本。