diagram-design:用代码把架构图变成可维护的工程资产
先聊点背景。做技术这几年我发现一个很有意思的现象很多团队聊起代码、聊起架构嘴巴上都头头是道可一到要画图讲清楚的时候就集体卡壳。有人打开白板画半小时有人用画图工具拖了一下午结果做出来的架构图要么丑得没法看要么逻辑乱成一团最要命的是——代码改了图还留在上次重构之前的样子成了团队里最不靠谱的“文物”。我自己也踩过不少这种坑。后来慢慢琢磨出一套用代码画图、把图表当工程来管理的思路也就是标题里写的 diagram-design。简单说就是以纯文本方式定义各种图表再借助自动化工具把它们渲染成可发布的图形同时纳入版本管理和持续集成。这套玩法最大的价值是把“画图”从手动劳动变成了可维护、可复用、可协作的工程资产。这篇文章就把我在 diagram-design 这条路上攒下来的经验完整拆开聊适合那些想把架构图、流程图、时序图画得更专业或者正在为团队文档协作头疼的人。1. 整体设计与思路拆解先回答最核心的问题为什么非要用代码来画图而不是像大多数人那样直接用画图工具拖拽1.1 手动画图的三个深坑我相信用过专业绘图软件的人对下面这三个场景绝对不陌生。首先是版本管理问题。一张架构图最初只有一个节点后来变成二十个节点再后来整个网络拓扑都改了。你用画图工具保存的文件往往是二进制格式或者私有格式放在 Git 里根本没法 diff。想看看这版相比上一版到底改了哪里除了睁大眼睛慢慢比对没有任何办法。就算某些工具支持导出 SVG 或者文本描述那也不是给人看的格式diff 出来全是天书。其次是协作效率问题。团队里五个人同时维护一张大架构图如果没有专门的服务端协同方案结果就是每个人都从网盘下载一个副本改完之后传到另一个命名里文件名从v1_final.mmd到v1_final_v2_真的最终版.drawio一路失控。最后谁也不敢删文件列表里的任何东西因为根本不知道哪一份是当前真正在用的。最后是复用问题。一张图里画好的主题色、边框样式、节点模板想复用到另一张图在图形工具里往往需要手动去复制样式跨文档复制还不一定生效。如果团队里强调统一的视觉规范这个操作就成了彻头彻尾的体力活而且永远有人偷懒不遵守。1.2 用代码定义图表的工程优势把图和代码放在一起思考就完全是另一个模式了。第一一切皆文本。文本天然支持 Git 版本管理每次改动都能通过 diff 清楚地看到增加了什么节点、删除了哪条连线、修改了什么样式标签。代码评审的时候可以像 review 业务代码一样 review 架构图描述这在传统画图工具里是不可想象的。第二构建自动化。写一个脚本把.mmd文件、.puml文件批量渲染成 PNG、SVG甚至直接推到团队的知识库或者部署到静态站点。整个过程没有人工环节也就消灭了“图没更新”这个最常见的团队矛盾。第三标准和规范可落地。既然图是文本那我们可以给它指定 lint 规则、既定模板、包结构和命名规范。团队提交前跑一下检查脚本不符合规范的图直接拦截在合并请求之外视觉一致性问题从根源上解决。我自己的体会是diagram-design 的核心不是某个具体软件而是一套思维模式把图表当作一类特殊的代码资产给它配套的工具链和工程流程。这样图就不再是某个人的临时产物而是团队协同的基础设施。2. 核心技术选型说完了思路落到实操上第一个要解决的就是用什么“语言”来写图。我在这个项目里前后对比过好几个主流方案这里讲讲我的选型过程和最终结论。2.1 渲染引擎对比Mermaid、PlantUML、Graphviz市面上的文本画图方案很多最常被拿出来比较的是这三个维度MermaidPlantUMLGraphviz核心定位面向文档嵌入的快速图表面向UML建模的完整方案面向图形结构的自动布局引擎标记语言Mermaid语法接近自然语言PlantUML语法关键词明确DOT语言语义丰富主要图类型流程图、时序图、甘特图、状态图、饼图等UML用例图、类图、序列图、组件图、部署图等树形图、关系图、双向图、大图集群布局渲染表现形式轻量、简洁适合Markdown嵌入复杂度高时可控性更好擅长处理大量节点与边的自动排版中文支持较好默认主题可正常显示需要额外配置字体否则中文乱码可通过配置字体解决生态集成GitHub、GitLab、VSCode均原生支持与Java/Swagger等工具链集成紧密老牌稳定Python/R等均可调用从表格里能看出来这三个工具的侧重完全不同。Mermaid 的优势在于“轻”和“顺手”只要你的核心场景是画工作流、架构分层、状态切换这种图它基本开箱即用。PlantUML 更偏“正统的软件设计图”在需要严格表达类之间继承关系、接口实现、软件部署结构的场景里它那套贴近 UML 语义的语法更让人有安全感。Graphviz 则更接近一个底层布局引擎它不是为你画图准备的而是为你那句“把这项数据在一张图上呈现出依赖结构”的需求准备的自动化布局能力极强。2.2 diagram-design 的最终选型结论先说我的个人配置主体流程、架构图、团队知识库内嵌图全部用 Mermaid只有需要出严格 UML 类图的场景才切 PlantUML处理非常庞大的依赖关系图时会直接用 Graphviz。我不建议在团队里“一刀切”只留一个工具。硬要用 Mermaid 画上百个节点的复杂拓扑它布局能力会捉襟见肘反过来明明只是想画个简单的数据流转过程却要写一整套 PlantUML 的工程配置也是杀鸡用牛刀。最重要的是每一种选型都要服务于“文本可维护、渲染可自动化”这个原则。工具是手段不是目的。3. 实操过程与核心环节实现下面进入正题我拿一个非常典型的场景举例给一个微服务系统画一张架构图。从零开始把一个 Mermaid 文件的诞生过程完整过一遍。3.1 先从元结构想清楚这张图画什么很多人一打开编辑器就开始敲图层但我不建议这么做。画图之前先问自己三个问题这张图给谁看画到什么颗粒度想表达哪几条核心链路这三个问题分别对应了图的阅读对象、信息密度和表达重点。给老板看的系统全貌层次要粗给新同学上手用的模块关系图路径要细给稳定性演练用的故障链路图核心依赖和关键在于延迟位置其他信息一律精简。以微服务架构图为例我通常会先定义一个基础结构用户端Web、移动端、第三方开放接口接入层网关、负载均衡、鉴权模块核心服务层用户中心、订单中心、支付中心、库存中心数据层MySQL 主从、Redis 集群、MQ 消息队列辅助系统日志平台、监控告警、链路追踪分层定义清楚之后脑子里对这张图的主干信息已经有了线性脉络接下来才好落笔。3.2 将结构翻译成 Mermaid 代码打开一个空白.mmd文件先写结构骨架graph TD subgraph client[客户端接入层] A1[Web 前端] A2[移动端 App] A3[第三方 OpenAPI] end subgraph edge[接入网关层] B1[Nginx 集群] B2[统一 API 网关] B3[身份认证中心] end subgraph core[核心业务服务层] C1[用户中心] C2[订单中心] C3[支付中心] C4[库存中心] end subgraph data[数据存储层] D1[(MySQL 主从)] D2[(Redis 集群)] D3[(RabbitMQ)] end subgraph ops[可观测性平台] E1[日志系统] E2[监控告警] E3[链路追踪] end注意看这段代码的几个手法。graph TD表示这是一张自上而下布局的流程图TD 是 “Top Down” 的缩写。如果节点特别多横着放更舒服就改成graph LRLeft Right。这个方向选择其实是在控制视觉密度很多人忽略这一点导致生成的图超出了容器宽度。我用subgraph做了分组并且给每个分组起了简短、有意义的 id比如client、edge、core。分组的价值特别大它不只影响视觉上的“方框嵌套”效果更重要的是让图形的阅读逻辑有层次感。读者第一眼看到的是大的分区边界然后才关注内部具体节点这比所有节点平铺在一个平面上要清晰得多。加了分组之后还要给节点加样式区分。不同层的节点用底色和边框进行视觉区分%%{init: {theme: default, themeVariables: {fontSize: 16px}}}%% graph TD subgraph client[客户端接入层] A1[Web 前端] A2[移动端 App] A3[第三方 OpenAPI] end classDef clientFill fill:#E3F2FD,stroke:#1565C0,stroke-width:2px; classDef coreFill fill:#E8F5E9,stroke:#2E7D32,stroke-width:1px; classDef dataFill fill:#FFF3E0,stroke:#EF6C00,stroke-width:1px; class A1,A2,A3 clientFill; class C1,C2,C3,C4 coreFill; class D1,D2,D3 dataFill;这里最关键的是classDef和class这两条命令。classDef可以定义一组样式规则然后用class命令把规则绑定到对应的节点 id 上。当一张图里有几十个节点时这个机制避免了逐个给节点写 style 的重复劳动同时保证了同类型节点样式统一。想改某个分区的配色只需改一行classDef即可。样式类的写法是fill: 背景色, stroke: 边框色, stroke-width: 边框粗细。颜色值要用十六进制或者标准的颜色名实际用下来十六进制更准确不会出现不同渲染器对颜色名理解不一致的问题。3.3 连线设计的隐藏门道画完结构下一步是连线。连线不是简单的“箭头对接”它的语义非常关键。graph LR A[用户] --|发起请求| B[接入层] B --|转发| C[业务服务] C --|读写| D[(数据库)] C -.-|异步事件| E[消息队列]这段代码展示了两种连线的写法--实线箭头表示强依赖常用于同步调用的主链路-.-虚线箭头表示弱依赖或异步非核心链路连线上的标签|发起请求|也很重要。给每条连线标注“干什么”图的阅读成本会骤降。很多人画完图阅读体验很差往往不是节点定义得不好而是连线没有语义。箭头根本表达不出“是哪种关系”——是写入、读取、还是调用这时候连线标签就起到了注释的作用。还有一个细节标准字面语法中长文案标签会撑开整条线段的长度布局容易变乱。解决办法是给节点定义短一点的别名然后把详情放到节点的 description 区域或单独的知识文档里去。图中只放核心动作不要放整个业务流程说明。这跟写代码时“函数名起短一点细节封装进函数体”的思路是一样的。3.4 把渲染接入自动化管道写好的.mmd文件如果不能自动渲染价值就损失了一半。这里我用 Mermaid 官方的命令行工具mermaid-js/mermaid-cli安装后得到一个mmdc命令。在 Node.js 环境下一条典型的渲染命令长这样mmdc -i architecture.mmd -o architecture.svg -b transparent -w 1600 -H 1000参数含义分别是-i指定输入的.mmd文件-o指定输出的文件路径支持.svg、.png、.pdf-b设置背景色transparent表示透明背景方便嵌入深色文档-w和-H指定画布的宽高单位是像素如果你是 Python 技术栈也可以使用mermaid-py之类的封装库但底层调用的仍然是同一个命令行工具。走企业级流水线时更建议直接把渲染命令放进 CI 脚本里# 伪代码示例在CI中渲染所有mmd文件 for file in docs/diagrams/*.mmd; do mmdc -i $file -o docs/images/$(basename $file .mmd).svg -b white done这样做的好处是“图随代码改”。开发者修改了架构稿的 .mmd 文件PR 合并后流水线自动重新渲染所有图表网站的对应文档区不需要任何手工操作就同步更新了。你再也不需要去问“最新的架构图是谁在维护”——答案永远是“代码自己会更新自己”。3.5 团队里的规范约定工具链跑通之后剩下的是团队协作层面的事。我强烈建议在项目里建立如下几条约定每个图表一个独立文件文件名小写中划线命名如order-service-flow.mmd图内统一使用英文 id中文只出现在展示文本中至少要有subgraph分组禁止画出的图超过“一眼能定位核心链路的范围”连线必须有语义标签严禁无标签裸连线classDef统一定义样式严禁零散地使用style硬编码节点颜色提交 PR 时必须附带渲染后的效果截图或 SVG 对比这些规则看起来琐碎但它们是“把图当代码管理”的最后一步。没有规则图库很快就会变成无人敢动的泥潭有了轻量规则即使团队里来了新成员也能很快按照既定风格产出合规的图表。4. 常见问题与排查技巧实录再顺的方案跑起来总会遇到一堆意想不到的问题。这里把我在 diagram-design 实践中踩得最深的几个坑拎出来顺便附上排查思路。4.1 布局乱飞节点和连线挤成一团Mermaid 的自动布局引擎在节点数超过一定量级我个人经验大概 30 个就是一个门槛之后布局质量会直线下滑。节点排列紧凑到相互重叠箭头莫名其妙绕了远路。遇到这种情况如果还想保留这张大图我的做法是拆图。拆图有几种策略。最简单的按子功能域拆比如order-domain.mmd只画订单相关的上下文依赖payment-domain.mmd只画支付链路的关联关系。另外一种是按层级拆把一张全局大图拆成“总览图 分域详细图”总览图只保留域间依赖和核心链路每个域的细节再单独出一张。拆分后总图失掉了一些细节深度但读者不会一头扎进 60 个节点的汪洋大海里找不到出口。这是“少即是多”在图表设计里的体现。实在不能拆的大图我才会考虑切 Graphviz 来处理。Graphviz 对大型图结构的布局算法更成熟但代价是定制化表达不如 Mermaid 便捷。4.2 中文显示乱码或出现方块很多新接触这类工具的人第一个问题就是“为什么我的中文全是方块”这个问题在 PlantUML 里尤其常见因为它的默认字体不一定支持中文字符集。解决思路也很直接Mermaid 一般不会乱码如果乱码优先检查渲染引擎所在环境的 locale 设置PlantUML 在服务端渲染模式下需要在配置里指定中文字体比如skinparam defaultFontName Microsoft YaHei如果是纯本地渲染确保系统中已安装对应中文字体而不是依赖字体连字或 fallback如果是在 CI 环境里批量渲染最容易忽略的一点是构建镜像不带中文字体包。本地看着好好的图一到 CI 出来的 PNG 全是豆腐块。这种情况需要在 Dockerfile 里显式安装字体比如基于 alpine 环境跑apk add font-noto-cjk或者 debian 环境跑apt-get install -y fonts-noto-cjk。4.3 渲染结果和本地不一致还有一类问题是“我本地渲染很好看怎么推到 CI 之后就变样了”。排查优先级如下确认本地和 CI 环境中的 Mermaid CLI 版本完全一致。不同版本之间的布局算法差异极大甚至语法支持都不相同这是绝大多数不一致问题的来源。确认 CI 环境里安装的字体与本地一致这会影响节点的宽高计算进而影响整体布局。如果 SVG 类文件出现微小的坐标偏移通常不影响阅读但如果做像素级比对建议统一锁定官方 Docker 镜像版本。在 CI 配置中锁死mermaid-js/mermaid-cli的精确版本号是最一劳永逸的做法。4.4 一个调试图的通用调试顺序最后分享一个我平时排查图的固定顺序你可以直接拿去参考。先检查语法把.mmd文件丢进官方的 Live Editor看能否正常渲染。如果 Live Editor 也挂基本是语法问题根本不需要猜布局。再检查节点 id 冲突同一个 id 出现在多台 subgraph 下会导致样式作用预期之外的地方用全局搜索排查重复。接着检查样式类绑定classDef写对了不代表class绑定对了绑错了节点视觉上会非常明显但也有可能两个节点恰好样式差不多容易看漏。最后检查渲染环境如果语法、 id、绑定都没有问题基本可以确定是环境差异问题回到前一条去比对版本。按着这个顺序走大多数疑难杂症都能在几分钟内锁定原因而不是对着生成的 SVG 空翻源代码。5. 扩展方向把图表资产盘活工具链和踩坑聊完最后再分享几个我后续在这个思路上做的扩展算是把 diagram-design 从“画图”本身往外延展盘活整份图表资产的价值。第一个扩展是架构图模板化。我把常见的微服务分层、网关接入、数据处理管道都沉淀成了团队内部的模板仓库。新项目启动的时候复制模板改一改节点名就有了第一张符合团队规范的架构图。这个效率提升非常明显过去需要一两天打磨的图现在半小时就成型了。第二个扩展是图与代码的联动检验。我尝试过写一个简单的脚本解析 Mermaid 图里的服务节点名再去 Kubernetes 的 Deployment 列表里核对实际部署的服务是否存在。如果代码描图与实际环境不一致说明文档已经落后于线上环境了这时候就自动生成一个“文档漂移”的提醒。虽然这个脚本还很粗糙但它代表了一个正确的方向图不该是静态的文档它应该能反映系统真实状态。第三个扩展是把图表嵌进自动化评审的流程里。我们团队的架构评审材料里可以直接插入由 .mmd 文件渲染出的 SVG 图评审人点开图就能看到链接到对应代码仓库的标注信息。图与代码从物理层面分离却又在阅读体验上无缝衔接这比贴一张死图片再附链接的方式友好太多。这个项目的后续演进空间其实很大关键在于你愿不愿意把图表从“顺手画的产物”升格成“认真维护的工程资产”。只要这个心态转了工具选型、规范制定、流程搭建都只是顺水推舟的事。