diagram-design这个项目,往小了说是“用代码画图”,往大了说是把图表从一次性的静态产物,变成可以版本的、可复用的工程资产。我最早碰这个东西,纯属被鼠标拖拽折磨出来的——画个架构图,先是对齐框体就花掉半小时;等需求一变,又得手动删线、挪位置、重新截图;最崩溃的是图上内容和线上系统的真实情况早就对不上了,但根本没人记得更新。换成代码化设计之后,图表跟着文档一起走,改起来就是改几行文本,渲染交给工具,几乎不用操心排版。这篇就围绕diagram-design的完整工作流来聊,涵盖方案选型、语法细节、实操案例、踩坑记录和团队落地经验,适合要写技术文档的开发者、需要梳理系统方案的架构师,以及任何想把复杂逻辑一次性讲清楚的从业者。
1. 先搞清楚思路:diagram-design到底在设计什么
1.1 为什么鼠标拖拽画图总是不靠谱
很多人以为画图难在“会不会用工具”,其实难在“图是要跟随逻辑变化的”。用拖拽式工具画图,你画的不是逻辑,而是一堆坐标。坐标是视觉概念,逻辑是结构概念,这两者一旦错位,图就成了死图。
我自己常见的翻车现场是这样的:流程图里新增一个判断节点,手动把后面的连线一根根拆掉重新接,然后把整块区域往右推,害怕挤到旁边的子图;子图内部的节点改了名字,外层的框线又得跟着拉伸,拉伸完布局全乱。这个过程至少重复三遍以上,等到第N次微调时,我已经不关心内容了,只想赶紧结束。
还有一层更隐性的成本:图片和文档是分离的。代码仓库里的README嵌了一张架构图,但架构图早已过期;方案文档里放了一张时序图,时序图对应的代码却是上一版逻辑。时间一久,图不仅没有辅助理解,反而在误导新人。
diagram-design的思路恰好绕开了这些问题。图的本质是结构化的信息,而结构化信息天生适合用文本描述。一旦用文本描述,就可以进版本库、可以做diff、可以被程序校验、可以被团队review。图形只是这个文本结构的一种“渲染视图”而已。
1.2 方案选型背后的核心逻辑
代码化画图这条路,不同工具解决的是不同层次的痛点。我简单梳理下当前几个主流选择。
Mermaid是目前最轻量的方案,语法接近自然语言,对Markdown生态的侵入性最小。写博客、写GitHub仓库、写内部知识库的时候,直接内嵌一个mermaid代码块就能渲染,不需要额外服务。它对流程图、时序图、状态图、甘特图支持得都不错,社区活跃度也高。
PlantUML更贴近UML语义,写类图、用例图、活动图这种专业建模场景时表达力更强,适合技术体系比较重的团队沉淀设计文档。代价是渲染通常依赖Java运行环境,在自己电脑上没问题,但在线平台和某些文档系统上接入成本会高一些,轻量场景下反而有点重。
Graphviz靠的是自动布局引擎,擅长处理依赖关系图、调用链、数据流这类结构复杂的图。你只需要声明节点和边的连接关系,布局算法自动安排位置,这一点对“节点特别多”的图来说极其好用。但它的默认样式确实寒碜,而且想做到视觉好看,需要花功夫调参数。
我的选型原则很简单:图的类型决定工具,而不是反过来。日常做业务流程图、系统架构图、时序图,优先用Mermaid;涉及UML类图和状态机建模,考虑PlantUML;处理几十个节点以上的依赖关系、血缘关系,就交给Graphviz。
那diagram-design这个项目具体怎么落地?我后面就用Mermaid作为主线,因为它最容易上手,也最能说明“代码驱动设计”的核心思路。PlantUML和Graphviz的思路完全一致,差异只是语法形态和使用场景。
2. 设计图表前必须搞懂的基础元素与原则
2.1 节点、连线、分组:图表就是结构化的表达
我习惯把一张图拆成三类基础元素来理解。
节点代表实体,可以是一个服务、一个人、一个状态、一个步骤。Mermaid里节点常用方括号写,例如A[用户]就是名为“用户”的节点。连线代表实体之间的关系,箭头表示流向或依赖,标签表示关系的语义。比如A -->|发起请求| B,意思是A把请求发给B。分组代表系统的边界和模块归属,Mermaid里用subgraph来声明子图,把同一层级的服务放在同一个框里,这样图的层级感立刻就有了。
这三类元素的关系,可以类比成一篇文章:节点是名词,连线是动词,分组是段落。图要表达的不是“有哪些东西”,而是“这些东西之间发生了什么、谁依赖谁、谁在哪个边界内”。
表格式对比一下不同工具的元素表达能力:
| 图表元素 | Mermaid | PlantUML | Graphviz |
|---|---|---|---|
| 节点形状 | 方框/圆角/菱形/圆形 | 类/用例/状态等多种 | 通用节点+形状 |
| 连线类型 | 箭头/虚线/加粗/双向 | 继承/实现/依赖/关联 | 有向/无向/虚线/实线 |
| 分组 | subgraph子图 | package/namespace/包 | cluster子图 |
| 布局控制 | 方向和间距参数 | 基础布局、偏强制 | 多种算法可切换 |
无论用哪个工具,设计阶段都建议先把节点、关系、分组这三张清单列出来,再动手写语法。清单列清楚了,图已经在脑子里成型了,写码只是翻译过程。
2.2 布局方向与阅读顺序直接影响表达效果
布局方向不是随便选的。横向排布(LR)适合表达时间线、处理流程、串行步骤,因为人的阅读习惯是从左到右,箭头天然指向“下一步”。纵向排布(TB)适合表达层级、包含关系、从上到下的命令分发,比如网关到服务的调用链就非常适合自上而下展开。
我踩过的一个典型误区是:把所有节点一股脑往上放,指望工具自动给出好布局。实际上,方向参数只是给出一个倾向,还是要靠你在设计时规划主路径。主路径永远是图的核心阅读线索,次要关系可以放在两侧,避免连线交叉。
另外,节点的ID要有语义。Mermaid里的ID是最终连接关系的锚点,起得乱七八糟,过两个星期连自己都看不懂。我习惯用业务代号加下划线命名,比如order_svc、user_frontend,既简短又能看出含义。ID和显示文字是分开的,显示文字可以用中文,ID保持英文,两全其美。
2.3 颜色和样式是语义的一部分
diagram-design对样式的基本要求只有一个:颜色要表达“类别”,而不是表达“喜庆”。一组图里,相同语义的节点头颜色应该一致,比如外部系统用灰色、核心服务用主色调、数据库用另一种色、异常路径用红色虚线。
Mermaid里可以通过classDef来统一定义一组节点的样式,再用class把某个节点归入这组。例如下图中的order_svc会被应用上名为core的样式,背景变成蓝色,字体变成白色。
classDef core fill:#1f6feb,color:#fff,stroke:#1f6feb,stroke-width:2px; class order_svc core;这样做的核心价值在于:视觉信息与逻辑语义绑定,读者扫一眼颜色就能判断“这句话是讲主要流程还是旁路分支”,不需要逐个读文字。全图一塌糊涂全是高亮,就等于全图没有高亮。颜色种类建议控制在四种以内。
字体的处理同样关键。尤其中文环境下,不同机器的默认字体不同,渲染出来的效果天差地别,严重的会变成豆腐块。在Mermaid里可以通过初始化配置统一指定字体:
%%{init: {"theme": "base", "themeVariables": {"fontFamily": "Microsoft YaHei, PingFang SC, Noto Sans CJK SC"}}}%%我一般会在靠前的位置就把这个配置写好,后面所有图表共用同一个主题基座,整体风格才统一。
3. 从零到一:设计一张完整架构图的实操过程
3.1 动手写代码前,先画一张“信息手稿”
很多人在代码化画图上手失败,是因为跳过了信息梳理这一步,直接开写语法。结果写到一半发现漏了一个服务、少了一条关系,回头补的时候布局全乱。
我的习惯是先在纸上或者白板上把信息结构画个粗糙版本,不需要好看,只需要把以下四件事定下来:
- 有哪些实体需要出现在图上?
- 它们之间是什么关系?谁调用谁?谁依赖谁?谁的数据流向谁?
- 哪些实体属于同一个模块或子系统?
- 主阅读方向应该是什么?
拿一个常见的“用户下单流程”来举例。先列出实体:用户、前端页面、API网关、订单服务、库存服务、支付服务、消息队列、数据库。关系大概是用户操作前端,前端请求网关,网关分发到订单服务,订单服务依次调用库存和支付服务,同时读写数据库,系统把事件发到消息队列。分组显然是把订单、库存、支付放进一个“业务服务”的子图里,外部实体放在外面。方向选择TB,A表示从上到下,符合“用户从顶部进来、系统在下方响应”的直觉。
3.2 从信息结构到Mermaid语法,逐行写出来
信息手稿一旦成型,写代码就是翻译过程。下面是一版可以直接跑起来的示例:
%%{init: {"theme": "base", "themeVariables": {"fontFamily": "Microsoft YaHei, PingFang SC, Noto Sans CJK SC"}}}%% flowchart TB U[用户] F[前端页面] G[API网关] subgraph 业务服务 O[订单服务] I[库存服务] P[支付服务] end DB[(数据库)] MQ[(消息队列)] U -->|访问| F F -->|提交订单| G G -->|路由请求| O O -->|校验库存| I O -->|发起支付| P O -->|持久化| DB I -.->|扣减完成事件| MQ P -.->|支付成功事件| MQ逐行解释一下:
flowchart TB声明这是一张从上到下的流程图,框架贯穿全图。节点定义里,[数据库]前面加了一对双括号[( )],这是圆柱体,表达数据库语义。箭头写法分两种:-->实线箭头代表直接调用,-.->虚线箭头代表异步事件。箭头后接|文案|,给关系本身加上语义标签,这是图读起来通顺的关键。
观察这张图的结构:主流程从上到下依次是用户、前端、网关、订单服务,之后再分叉到库存和支付。信息层次比一开始手稿更清晰,因为分组让“哪些服务属于业务域”一目了然。
3.3 样式定制:用主题变量和classDef控制全局风格
默认渲染效果虽然能看,但在正式文档里通常不够精致。Mermaid的定制入口有几个层次:
第一层是全局主题变量,可以改背景色、字体、阴影等基础属性。第二层是classDef批量定义节点样式,适合给某类节点统一配色。第三层是linkStyle,可以单独设置某条连线的颜色、粗细和虚线属性。
比如我想把订单服务突出显示,同时把两条异步事件链路做成虚线弱化,可以这样追加:
classDef core fill:#1f6feb,color:#fff,stroke:#1f6feb,stroke-width:2px; class O core; linkStyle default stroke:#888,stroke-width:1px;这里的逻辑是:大部分连线设为浅灰色,只让核心调用路径保持正常,整个图的视觉重心就会自然落在关键结构上。样式调整的边界是“每一步改动都要有理由”,如果只是为了花哨,建议全部不要改。
3.4 渲染、导出、嵌入文档:完整闭环
写完语法后,到真正“出图”还有一段路。我通常用的流程是:
先在Mermaid Live Editor里粘贴代码,看渲染效果。这里迭代最快,改完秒出图,适合日常调试。确认没问题后,如果要输出成SVG或PNG,就用mermaid-cli完成:
mmdc -i architecture.mmd -o architecture.svg -b white --scale 2参数含义:-i指定输入文件,-o指定输出文件,-b设置背景色为白色,避免透明背景在文档里显得突兀,--scale 2输出两倍分辨率的图像,插入文档后放大也不会糊。
如果图要嵌进Markdown,直接用三个反引号包住mermaid代码就行,GitHub、GitLab、语雀、Notion这类平台基本都可以原生渲染。如果图要放进Word或PPT,就导出PNG或SVG再插入。SVG的优势是矢量和可编辑,缺点是部分老版本办公软件支持不好,这点要自己权衡。
整个流程走下来,从信息手稿到最终成图,大概不超过十分钟。改版时同样从文本改起,重新导出即可,不再有“手动挪坐标”的工序。
4. 高频问题与排查技巧实录
4.1 箭头方向画反了,而且自己反复看不出来
问题是这样的:明明在代码里写的是A --> B,再去看渲染图,总感觉箭头指向不对。原因在于箭头语义是“从A指向B”,如果你对“谁调用谁”的定义和对箭头的默认预期刚好相反,就会觉得反了。
排查思路很简单:先确认箭头方向约定。A --> B,箭头的尖锐端在B这一侧,意思是A访问B。然后再确认业务语义,比如“前端调用网关”和“网关接受前端请求”是同一件事,写F --> G就对,写G --> F就画反了。
建议是:动手写码前统一一个约定,全项目一律使用“调用方指向被调用方”。这样任何时候看到箭头就能直接读成“左边调用右边”,不用每一条连线重新理解语义。
4.2 同一份代码,本机渲染正常,线上变成豆腐块
这种情况几乎都是字体问题。线上渲染环境里没有你本地安装的中文字体,渲染系统回退到默认字体,而默认字体又不支持中文字符集,结果就是一堆方框。
解决手段就是塞fontFamily配置,在前面已经给过示例。关键是配置里的字体列表要覆盖几种主流系统,否则只写一个“Microsoft YaHei”,在macOS和Linux服务器上还是回退。
如果走mmdc导出图片,还可以用css注入的方式强制指定字体:
mmdc -i input.mmd -o output.svg --css "text { font-family: 'Noto Sans CJK SC' !important }"这个方法胜在精确,适合线上环境没办法改初始化配置的边界场景。
4.3 节点一多,连线交叉,图变成蜘蛛网
单图超过四五十个节点的时候,Mermaid的布局引擎容易在密集交叉的连线上失去优雅感。这时候不要试图用参数微调解决,那是错误的方向。
我用过最有效的三个办法:
- 把大图拆成多张小图,每张图只讲一个主流程,小图之间用文档的章节逻辑串联。
- 增加subgraph分组,让相互关联的节点先成小簇,再让小簇之间连线。子图结构能显著减少跨区域连线的交叉概率。
- 调整方向参数,比如LR换成TB,或者反过来。有时候布局密密麻麻并不是结构问题,只是方向选择不适合这张图的形态。
实践下来,一张图控制在二十到三十个节点之间,可读性和可维护性最好。
4.4 导出的图片不够清晰,插入文档有锯齿
我最初遇到过从Live Editor截图出来的图片,插到PPT里一放大就糊成一片。后来换成mmdc导出,问题就解决了。关键参数就是--scale,需要多高的清晰度就设多大的倍率,常用2到3就够。
如果对清晰度有极致要求,可以考虑输出SVG而不是PNG。放大到任何程度都不会失真,也方便后续二次编辑。缺点是GitHub等平台上预览SVG不如PNG稳,具体情况需要你自己按平台取舍。
4.5 常见问题速查表
| 问题现象 | 主要排查方向 | 建议解法 |
|---|---|---|
| 箭头方向和预想相反 | 箭头定义的方向约定 | 统一“调用方指向被调用方”,统一使用A --> B写法 |
| 中文显示为方块 | 字体缺失或字体未指定 | 在init中配置multi-fontFamily;导出时用css强制指定 |
| 节点多、连线乱 | 节点数量与布局参数不合理 | 减少单图节点数、增加subgraph、切换方向 |
| 导出的图模糊 | 图片分辨率不足 | mmdc加--scale 2/3;业务上改用SVG |
| 子图框位置奇怪 | subgraph与节点定义顺序 | 先定义节点,再写subgraph归属,别交叉引用 |
| 语法报错但看不出错在哪 | 某个特殊字符没转义 | 检查标签里是否含引号、括号,必要时用引号包裹 |
这几条是我实际跑diagram-design项目时遇到最频繁的问题,基本覆盖了从语法到渲染、从字体到导出的全部高发故障。
5. 团队协作时,diagram-design怎么落地
5.1 图表进代码库,让图和逻辑同源
个人用图,很大程度上是给自己看;团队里用图,就得考虑“图有没有人维护”。diagram-design最大的优势此时才真正显现:图是文本文件,可以放进代码仓库,跟着项目代码一起走。
这意味着改图就像改代码一样,有修改记录,有diff检查,有review过程。某一个服务下线了,删掉它对应的节点定义和连线,PR里一目了然;某条链路新增了依赖,加上节点和箭头,reviewer可以看到改动范围。过去那种“文档图过期了三个月没人管”的问题,在代码化图表面前不存在——图改不动的地方,和代码没法编译是一个道理。
我现在的做法是,每个系统模块的docs目录下放一个diagrams文件夹,里面按图的功能命名,比如checkout-flow.mmd、payment-seq.mmd。Markdown文档里用相对路径引用渲染后的SVG或者直接内嵌mermaid代码块,保证图和文档永远跟随主分支同步演化。
5.2 沉淀团队自己的diagram-design规范
用了一段时间之后,我强烈建议每个团队沉淀一份简易规范,不用长,就几页纸,但能省掉大量协作成本。规范至少应该包含这几项:
| 规范项 | 推荐约定 | 理由 |
|---|---|---|
| 节点命名 | 英文小写加下划线 | 编码稳定,不随显示文字改动而断链 |
| 拼音英文 | 业务名词优先用常见英文缩写 | 避免中文ID在部分渲染环境出问题 |
| 方向默认值 | 流程图用LR,层级图用TB | 符合阅读习惯,减少随意性 |
| 颜色语义 | 蓝色=核心,绿色=成功,橙色=告警,灰色=外部 | 颜色承载语义,避免风格分裂 |
| 子图命名 | 统一用业务域名称 | 区分层级归属,便于检索 |
| 标签风格 | 箭头上的文字用动词短语 | 表达“关系”,而非“状态” |
这些规范看起来琐碎,但真正运行几周后会体会到它的价值:别的新人接手看图,不用猜颜色什么意思;你回看自己三个月前画的图,也不用努力回忆当时想表达什么。
另外可以顺带在CI里加一道mermaid语法校验,用mmdc的--check参数在提交时跑一遍,语法错误直接拦在合并之前。这跟代码检查一样,把低级错误挡在门口。
5.3 从“画图”到“设计”的思维转变
我用diagram-design的时间越长,越觉得“design”这个词才是重点。它不只是把图画出来,而是让你在设计阶段思考清楚信息层级、关系类型、阅读路径和风格一致性。
写代码画图这个动作本身没什么稀奇,真正有价值的是背后这套流程:想清楚结构,写成文本,让文本可维护。当你习惯了这种工作流,再回头用拖拽工具画复杂的架构图,会特别痛苦于那些无法自动对齐的框、无法批量修改的样式、无法进版本库的图片。
最后分享一个小技巧:如果你手头有一段要反复调整的图表代码,先在Live Editor里调好整体结构和样式,确认逻辑没跑了再落到本地仓库。本地仓库永远只保留“可发布的、被review过的”版本,别把调试过程中的各种半成品全塞进去。这样你的diagram-design资产才会越积累越干净,而不是又变成一个存放混乱图片的新仓库。