1. 项目概述与核心思路
1.1 这个diagram-design想解决什么问题
工作里经常有这样一个场景:一张好不容易画好的架构图、流程图或时序图,过了一个月再打开原始文件,发现里面的逻辑已经改了三四轮,图里却还是旧版本。要么是画图的人懒得更新,要么是原始工程文件早就不知道丢在哪个共享盘角落。更头疼的是,一张图往往只有一个人能改,其他人想提个意见,只能截图加红圈,最后图越来越乱。我这次的diagram-design项目,就是想用一套“把图表当成代码来管理”的方式,把这些麻烦一次性解决掉。
diagram-design本质上是一套图表设计规范加配套的工程化实践:先选定一种文本化图表语法作为统一底层格式,再把所有图表文件纳入版本管理工具,配合自动渲染、自动校验和在线预览,让图表像代码一样有版本、有历史、可评审、可协作。它并不神秘,核心就一句话:你不再用鼠标拖拽画图,而是用几行结构化的文本描述出图的节点、连线、分组和样式,然后再由一个渲染引擎把它变成你想要的图片或网页。
我最早尝试这套思路,是因为团队里的架构文档实在维护不下去了。每季度评审时,大家拿着旧图讨论新方案,产生了不少误解。后来我花了一个周末把所有核心图表改成文本化描述,用上自动渲染之后,效果立竿见影:图改了哪里、谁改的、为什么改,全部清清楚楚。这个项目适合谁呢?比如需要长期维护接口调用关系图的后端开发,负责组织级技术方案沉淀的架构师,喜欢把流程规范文档化的技术管理者,以及所有被“画图两小时、改图一整天”折磨过的人。哪怕你只是个人写笔记,这套思路也一样能省下大量重复劳动。
1.2 为什么选择代码化图表这条路线
很多人一听到“代码画图”就下意识觉得麻烦。但我要先掰扯一下这里面的关键问题:传统拖拽画图工具真的更好用吗?短平快的场景下,比如临时画个示意图给同事讲思路,鼠标拖拽确实效率很高。可一旦图进入了“需要长期维护”的生命周期,传统工具的两个致命弱点就暴露了。
第一是版本追溯困难。大部分桌面画图工具的工程文件是私有的二进制格式,存进Git之后,哪怕只挪动了一个方块,diff也是一堆没有意义的乱码。你根本没法在代码评审里看出这次改动到底动了什么逻辑。
第二是协作门槛高。传统工具基本是“谁的文件谁说了算”,其他人要么等文件传来传去,要么只能截图提意见。就算用了在线协作画布,多人同时改动时也常常出现位置被乱拖、连线被弄断之类的混乱。而代码化图表把“图的本质”抽离成纯文本的逻辑结构:节点是什么,从哪里到哪里,在哪个分组里。这些信息本身就是人类可读的。于是Git能逐行比较、能标注评论、能实现多个分支并行修改,协作方式一下子回到了程序员最熟悉的节奏。
我还看中一点:文本化让“图的生成”可以自动化。你的架构图可以直接从Kubernetes资源清单生成,调用链路图可以根据接口定义文件自动绘制,网络拓扑图可以扫一圈云厂商的API自动拼出来。这基本是传统画图工具不可能实现的能力,但对diagram-design来说只是水到渠成。
2. 工具选型与基础规范
2.1 主流图表引擎选型对比
选工具是diagram-design项目的第一步,也是决定后续体验的关键一步。我试过市面上主流的几种文本化图表方案,各有优劣,这里直接给出一份我实测后的对比,省得你再踩一遍坑。
| 工具 | 适用场景 | 语法难度 | 渲染质量 | 协作能力 | 我的评价 |
|---|---|---|---|---|---|
| Mermaid | 文档内嵌图、流程图时序图甘特图 | 低 | 中上 | 极好(GitHub原生支持) | 首选,适合90%的场景 |
| PlantUML | UML专项:类图、时序图、用例图 | 中低 | 中 | 较好 | 传统UML场景依然能打,但更新偏慢 |
| Graphviz / DOT | 复杂拓扑、自动布局、大规模节点图 | 中高 | 中 | 一般 | 布局算法最强大,但语法比较反直觉 |
| D2 | 强调可读性的现代图表 | 低 | 高 | 较好 | 新起之秀,布局比Mermaid干净 |
| diagrams.net(开发模式) | 需要精确像素级控制 | 低 | 高 | 一般 | 可以存成XML文本,但diff不友好 |
我自己最终主要用Mermaid,因为它贴合Markdown生态,GitHub、GitLab、很多笔记软件都原生渲染,零额外成本。但如果你的项目里需要大量类图,PlantUML在某些细节上仍然更规范。如果图非常复杂、节点之间有大量交叉连线,Graphviz的自动布局算法往往能救你一条命。这里不多评价D2,它确实漂亮,但生态还不够成熟,团队推广成本偏高。
我给一个选型心法:先看“图的受众在哪里”。如果图最终要放在GitHub或企业Wiki上,Mermaid几乎没对手;如果图主要放进技术文档的PDF里,PlantUML对UML的标准支持会少很多麻烦;如果图是自动化生成的中间产物,Graphviz的DOT格式反而更适合被程序拼装。
2.2 一套能长期用的目录与命名规范
很多教程会让你直接开始写语法,但把diagram-design落到团队里,真正决定成败的是规范。没有规范,三个月后就是一堆graph-1.md、graph-v2.md到处乱扔,跟之前用Word存的图也没有本质区别。
我建议项目目录按“领域/图名/版本”三层来组织。领域对应图所属的业务模块或系统边界,图名只保留“要表达的内容”,版本则完全交给Git去管理,不需要在文件名里体现。比如我的一个实际项目里,目录长这样:
diagrams/ ├── user-service/ │ ├── login-flow.md │ ├── order-state.md │ └── internal-api.md ├── payment-service/ │ ├── transaction-flow.md │ └── settlement-state.md └── infra/ ├── network-topology.md └── deployment-architecture.md文件名统一小写英文加中划线,关键词能一眼看懂。每个文件头部加一个简短的元信息块,写清楚这张图的作者、维护人、对应代码仓库地址、以及最近一次更新时间。更新时间这里要解释一下为什么重要:diagram-design的好处之一是能自动渲染,但渲染不会告诉你图是否过期。我在每个文件顶部固定维护一行last-updated,一旦逻辑变了就顺手改掉,配合CI检查能提醒团队里的每个人。
命名这块还有一个容易踩的坑:不要用“最终版”这类形容词做文件名。Git的意义就在于“没有最终版,只有历史版本”。如果你发现团队里每个人都在文件名后面加v3、v4,说明还没建立代码化图表的习惯,先把这条立起来,效果立竿见影。
3. 实操全流程:用Mermaid画一张系统架构图
3.1 第一步:把图画拆成需求
别急着打开编辑器写语法。拿到任何一张图表需求,我习惯先花几分钟做一次“图的拆解”,明确三个问题:主体对象是什么、主体之间是什么关系、围绕主体有哪些附属信息。
以项目里常见的“订单服务架构图”为例。主体对象是订单服务本身、它依赖的数据库、缓存、消息队列,以及上游的调用方。主体之间的关系是调用、读写、订阅和发布。附属信息包括协议类型、端口、数据流向、故障隔离方式等等。把这些写在纸上,比直接打开编辑器边想边画高效得多。
我通常用一个简单的表格来约束拆解结果:
| 对象 | 类型 | 关系 | 备注 |
|---|---|---|---|
| 前端订单页 | 上游调用方 | 通过HTTPS调用订单服务 | 网关统一鉴权 |
| 订单服务 | 核心服务 | 接收并处理下单请求 | 无状态多副本部署 |
| MySQL订单库 | 数据库 | 服务通过JDBC读写 | 主从架构,读写分离 |
| Redis缓存 | 缓存 | 服务通过Redis客户端操作 | 缓存订单状态 |
| 消息队列 | 异步通道 | 服务发布订单事件 | 下游支付、积分监听 |
这个表格画完,图的基本信息就已经齐了。Mermaid里的每个节点都来自“对象”列,每条连线都来自“关系”列,分组方式要么按系统边界、要么按部署环境。值得强调的是,先拆对象再画图,能让你的图天然具备清晰的分层结构,而不是一团乱麻。很多人画图难看的根因,不是语法不懂,而是脑子里根本没想清楚图里应该出现哪些东西。
3.2 第二步:用代码把草图画出来
拆解完成后,就能进入实际的diagram-design编码环节。我在Mermaid里最常用的是flowchart,因为它表达能力最强、直觉也最好。一张订单服务架构图的初始版本长这样:
flowchart LR A[前端订单页] -->|HTTPS下单| B[订单服务] B --> C[(MySQL订单库)] B <--> D[(Redis缓存)] B -->|发布订单事件| E[消息队列] E --> F[支付服务] E --> G[积分服务]这五行已经建立起基本的图结构:从左到右,前端进来,订单服务落库、更新缓存、发布事件,消费方继续往下走。接下来需要做的是分组合格式化。真实系统里通常有多个服务、多个中间件,如果不分组,图会横向拉得极长。Mermaid的subgraph是这里的主角:
flowchart TB subgraph client[客户端层] A[前端订单页] end subgraph app[应用服务层] B[订单服务] end subgraph infra[基础设施层] C[(MySQL订单库)] D[(Redis缓存)] E[消息队列] end subgraph downstream[下游消费方] F[支付服务] G[积分服务] end A -->|HTTPS下单| B B --> C B <--> D B -->|发布订单事件| E E --> F E --> G不要小看这个简单的分组,它直接定义了图的视觉语言:横向上分出了清晰的层次,纵向上每个层次内部形成聚合。读者第一眼就能理解“订单服务处在中间位置,上游是前端,下游是基础设施和消费方”。我给这个项目定的一条铁律是:一旦节点数超过六个,就必须用subgraph分组,否则可读性会断崖式下降。
Mermaid对节点形状也有一些约定俗称的经验。方括号表示模块,圆括号表示内部函数或接口,花括号表示判断,双括号表示数据库。我自己的规范是:服务用方括号,中间件用数据库符号,外部依赖用圆角矩形。这样不用看图例,读者也能凭形状快速识别节点类型。
写到这里还有一个细节:连线上的标签,一定要写“动作”或者“协议”,不要写“依赖”这种没有信息量的词。A -->|依赖| B这样的图等于白画,改成A -->|HTTPS调用| B之后,信息量立刻提升。这是我在这类项目里抓review时最常提的一条意见。
3.3 第三步:评审、合并与版本管理
diagram-design项目的核心收益,在评审环节体现得最明显。以前大家看架构图,只能在会议上投屏讨论。现在有了文本化描述,所有人可以像看代码一样逐行提意见。我在项目里规定,任何图表文件的改动必须走分支合并流程,流程非常简单:新建分支、修改图文件、推送远程、发起Pull Request。
评审时重点看三块。第一,逻辑是否与技术方案一致。比如订单服务是否少画了一个依赖方,或者消息队列的订阅关系有没有画反。第二,命名和分组是否符合规范。第三,有没有不必要的复杂度。这里有个常见的坏味道:为了“让图看起来很完整”,把很多无关节点塞进来。我的原则是,一张图只回答一个问题,多余的信息全部拆出去。如果评审中有人问“这个服务为什么出现在这里”,八成就是多余节点。
合并之后,就需要设置自动渲染。最轻量的做法是直接在GitHub上依赖它原生的Mermaid渲染能力,.md文件打开就能看图。如果想要更定制化的效果,可以用脚本把Mermaid渲染成SVG或PNG,再作为构建产物输出到文档站点。我在项目里用的是后者,因为团队文档站里经常需要引用图片,渲染成SVG之后可以高清嵌进任何页面。
版本管理的细节上有一条我一直坚持:图表文件必须和对应的代码放同一个仓库,别单独建一个“文档仓库”。理由也很实际,图表是为了描述系统的某一部分静态结构,它应该紧随代码变动。代码改了,同一个Pull Request里就该有对应的图改动,这样才能保证图和实现永远同步。如果放在独立仓库,哪怕有自动化同步,也很容易产生几个合并请求之间的时间差,图照样会过期。
4. 进阶玩法与稳定性保障
4.1 大图拆分的三种姿势
把diagram-design真正用起来后,你迟早会碰到一个瓶颈:某张图越来越庞大,几百个节点堆在一屏上,渲染出来后密密麻麻像电路板,谁也看不懂。我的原则是,一张图超过十五个节点就属于“危险信号”,超过二十个节点基本一定需要拆分。
拆分不是随意的,通常有三种姿势。
第一种是按边界拆分。比如“整体架构图”太庞大了,就拆成“下单链路图”“结算链路图”“对账链路图”,每条链路只画跟自己相关的部分,公共组件用注释符号或者虚线框简单示意,不重复展开。
第二种是按层次拆分。先画一张一级分层图,把应用、中间件、外部系统画清楚,然后在另一个文件里单独展开“应用服务内部的结构图”。一级图是地图,二级图是街道,用标签互相引用,在文档里放上链接跳转。这种方式对新人理解系统最友好。
第三种是按交互流程拆分。针对那些状态特别多的对象,画“状态图”而不是把所有流程塞在一张图里。比如订单的状态机,可以画一个只有状态和迁移条件的图,把每个迁移条件里的调用细节拆到另一张“下单时序图”里。
我之前遇到过一个最极端的案例,团队里有一张800多个节点的网络拓扑图,是从配置自动生成的。这种情况下任何人工拆分都不现实,只能依赖第二种方式里讲的“按层次”:先按机房聚合,每个机房内部再按交换机层级递归聚合,最终每一层图片控制在几十个节点以内。
4.2 从结构化数据自动生成图表
diagram-design最大的想象空间,在于图表可以不再是“画”出来的,而是“算”出来的。我在这套项目里做的一个实践,是从数据库表结构自动逆向生成ER图。以前维护一份数据库关系图非常痛苦,表一多、字段一变,图就要手动改半天。现在只需要在CI里跑一个脚本,读取数据库当前的信息架构,拼装成Mermaid语法,然后提交到渲染流程。
这种“数据驱动图表”的思路有几个典型的落地方向。第一个是接口调用关系图:扫描微服务代码里所有REST调用,生成服务间的依赖图。第二个是Kubernetes资源拓扑图:读取集群里的Deployment、Service、Ingress对象,自动生成部署结构图。第三个是消息流图:扫描代码里所有发布和订阅的Topic,画出事件流转关系。
脚本的核心并不复杂,无非是“把结构化数据映射成节点和连线”。但有几个细节值得提一下。生成结果必须强制排序,否则节点顺序每次都不一样,Git diff会充满噪音。给每个节点加稳定ID,ID要基于对象的唯一标识,不要用自增序号。生成频率不要太高,否则图会像监控大屏一样不停变化,反而无法作为稳定文档沉淀。
我还试过用前后两次生成的图文件做diff,让代码评审只看到实际变化。这个体验相当不错:服务A增加了一个对服务B的调用,Pull Request里就只有一条新增连线。相比传统的架构评审,这种“有依据的图”可信度高了不止一个档次。
4.3 文档流水线里让图表永远不过期
图表过期,是几乎所有技术文档系统的通病。diagram-design给这个问题提供了一个还不错的解法:把图表生成放到文档流水线里,让它与代码构建同步进行。
我在项目中配置了一套简单的CI流程:代码push后,构建脚本先跑一遍测试和打包,然后再跑一次图表渲染流程。渲染流程分两步:先检查所有.md图表文件是否能语法解析通过,不通过就直接构建失败;再执行生成脚本,把需要导出的图渲染成SVG和PNG,复制到文档站点目录。
这套流程跑通之后,文档里出现“这张图已过期”的概率会大大降低。因为你的代码合并且构建成功的同时,图一定已经被重新渲染了。还有一条更严格的实践:在图文件里嵌入一个“标签”,记录它关联的源代码目录哈希。CI脚本会比较当前代码哈希与图里记录的哈希,不一致时打印警告。这个功能相当于给图表上了“保鲜期”,再也没人敢拿三个月前的架构图去汇报了。
当然,任何自动化都不是银弹。数据驱动生成的图,永远只能描述“现状”,画不出“目标架构”。真正需要体现规划和演进的图,依然要依靠人来画。所以我的建议是:把“描述现状”的图全部自动化,把“描述未来”的图交给人工,两者各自发挥优势,互不干扰。
5. 常见问题与排查技巧实录
5.1 语法报错排查
Mermaid的语法看似简单,但实际跑起来,还是会遇到不少玄学报错。我整理了几个高频问题,和对应的排查思路。
第一类是“语法解析直接失败,页面只显示错误提示”。这类问题绝大多数出在节点文本里的特殊字符上。比如节点标签里写了一个括号(),Mermaid会把括号理解成语法结构,导致解析器蒙圈。解决方案是给标签文本加引号,比如A["订单服务(核心)"]。还有带斜杠的文本,比如路径/api/v1,有时候不报错但渲染结果异常,同样建议用引号包住。
第二类是GBK字符集导致的中文乱码。这个在国内项目里尤其常见。检查你的文件编码是不是UTF-8,几乎所有现代编辑工具默认就是UTF-8,但如果文件是从Windows老版本复制过来的,可能带着BOM头,某些渲染器会对BOM处理不稳。我遇过一次比较刁钻的场景:在Windows上用记事本编辑了图表文件,提交到Linux的CI上构建,渲染出来第一行多了一个奇怪的字符。排查了很久才发现是BOM,后来给渲染脚本里加了一步去BOM的处理,问题彻底消失。
第三类是节点ID重复导致的连线错乱。Mermaid里,节点ID是唯一标识符,同名ID会被合并成同一个节点。如果你复制了一段代码,忘了改ID,两个逻辑上不同的节点会被画成同一个。这个最容易在“增加一个新节点”时发生,我的习惯是给每个节点ID加上有语义的前缀,比如srv_、db_、mq_,从源头上降低重名的概率。
5.2 中文字体和样式问题
diagram-design渲染出来的中文,在默认情况下往往不太美观。西文字体里中文字形适配不好,会显得发虚或者大小不统一。Mermaid本身不直接提供字体配置,但可以把配置项写进主题里。我在项目里是把字体设置为系统中文字体的标准序列,渲染到SVG之后,在网页上显示正常,导出PNG时需要确保服务器上安装了对应的中文字体包。
还有一个细节是“行高”和“字符宽度”。中文字符的全角宽度会比英文字符宽,如果节点边框是固定宽度,中文多一点就把形状撑破或者文字溢出。解决方法通常是在设计时给中文字符预留足够的空间,或者通过配置把节点文本的wrap打开,允许自动换行。这里的经验值是:8个中文字符建议设计宽度对应16个英文字符的尺寸,按这个比例预留就不会出大问题。
如果你要导出高清PNG,建议把渲染尺寸定大一些,比如scale: 3,然后再压缩。这样可以避免图片在文档里放大后出现锯齿。我曾经为了图省事直接按1倍导出,结果PPT投屏时图边缘全是毛刺,后来统一改成3倍导出,视觉质量明显提升。
5.3 布局与可读性优化
很多新人在diagram-design项目里画的图,逻辑完全正确,但看起来就是“不舒服”。问题往往出在布局上。第一个常见问题是一张图里方向混乱。flowchart要么统一LR,要么统一TB,如果一会儿从左到右、一会儿从上到下,读者的视线就来回跳,非常累。除非图有天然的层次结构,否则我建议全图只使用一种主方向。
第二个问题是连线交叉过于密集。交叉最多的地方,往往是两个分组之间“多对多”的关系。比如三个服务都操作同一个数据库,连线的交叉几乎不可避免。解决办法是引入一个中间节点,比如“数据访问层”,让服务先连到数据访问层,再连数据库。连线数量没有变少,但视觉上交叉大幅减少,可读性明显提升。
第三个问题是子图内部和外部的连线混在一起。Mermaid在给节点归类时,有时候会把连线的起点或终点错误地附着在subgraph边界上,导致出现了很多指向整个分组的连线。排查方法很简单,把连线的起点和终点都明确写成具体节点ID,不要用分组ID充当连线端点,绝大多数情况都能解决。
最后再分享一个我在实际操作里的习惯:每次合并图表改动之前,先打开渲染后的SVG,用“缩小到25%”看一眼整体效果。如果缩小后看不清任何一条业务链路,说明这张图还有优化的空间,必须拆或改,直到它“缩小也能看懂”。这个简单的自我检查,帮我挡住了很多次低质量图表的合入。