从拖拽到代码:diagram-design让架构图与代码同步演进
如果你所在的项目组里架构图还停留在“谁画了图谁负责更新”的状态那 diagram-design 这个概念值得你花点时间认真看看。简单说diagram-design 就是用代码来设计和维护图表——把架构图、流程图、时序图这些本该出现在文档里的东西变成源码仓库里的一份文本文件。我第一次从 Visio 切到纯文本画图时内心其实很抗拒直到某次评审会上需要把整套系统从单体切换成微服务视角我只改了一处定义、重新执行了一次命令全套图就刷新完了从那以后我再也没回头过。这篇文章我会从思路、工具、实操到踩坑完整讲一遍我搭建 diagram-design 工作流的全过程适合后端、前端、架构师也适合所有被文档图折磨过的人。1. 为什么我做 diagram-design 而不是继续用拖拽画图1.1 传统画图工具的成本被低估了很多人觉得画架构图嘛打开 Excalidraw、Visio 或者 ProcessOn拉几个框、连几根线半小时搞定何必折腾代码我原来也是这么想的直到项目进入持续迭代期才发现图形界面画图有一个特别隐蔽的坑图一旦多起来改图成本是线性上升的。最常见的情形是这样的第一版架构图画得很漂亮发给团队评审通过。过了两周某个服务拆分出去了缓存中间件换掉了消息队列加了一个 Topic。你打开源文件准备改结果发现当时为了对齐框框手动拉了半天的坐标拖一下这个方块整条线就歪了连线的折点全部乱掉。改完局部之后为了保持整张图美观又要花十分钟去调布局。这还只是单人维护。如果一张架构图由多人协作两个人同时打开一个文件互相覆盖改动那简直是一场灾难。我在一个跨团队项目里经历过最离谱的情况因为没人愿意维护架构图最后评审时看到的图是三个月前的和线上真实架构差了三个服务。这种事情只要发生一次团队就会对文档彻底失去信任。传统拖拽画图看起来成本低实际上它的成本发生在每一次改图、每一次评审、每一次服务变更之后。单个图的绘制成本或许不高但乘上一个长期维护的系数成本会大得惊人。1.2 代码画图的四个直接收益diagram-design 的核心是把图变成代码也就是把图表当作软件工程的一部分来管理。它带来的收益不是画图更快而是让图和代码同步演进。我用下来最直接的收益有四个每一个都切中真实痛点。第一是可版本化。图源文件是纯文本就能放进 Git每一次修改都有 diff、有提交记录、有作者信息。评审时不需要说“你打开这张图看看”而是直接看改动行数哪条线连到了哪个服务一目了然。第二是可自动化。图既能手工执行命令导出也能接入 CI/CD 流程在每次文档更新时自动重新生成图片保证图表永远是最新状态。这一点对技术文档库特别重要人总有惰性自动化能抵消这方面的成本。第三是可嵌入。用代码写的图源文件可以直接内嵌到 Markdown、Confluence、语雀等文档平台中不用再截图上传。流程变了重新跑一次构建图片跟着更新完全避免了“文档里的图和代码不一致”的死角。第四是可复用。一段 Mermaid 或者 D2 的代码可以定义成模板在多个文档中复用。比如统一的系统边界容器、统一的颜色规范、统一的节点样式定义一次全局生效。这是拖拽画图工具很难做到的。1.3 别指望它替代所有场景当然我也得泼点冷水。diagram-design 不是万能的有些场景下硬上代码反而自我折磨。比如说头脑风暴阶段大家在白板或者平板上自由地画气泡、画箭头、写便利贴这个阶段的手感和自由度是代码无法替代的。又比如面向客户的高保真方案图需要很强的视觉表现力讲究配色、阴影、渐变、图标细节直接用代码生成会显得很“工程感”不够“设计感”。我通常会先用手绘工具快速画出草图确认好结构和逻辑之后再落到代码化图表工具里做正式版本。还有一个容易被忽略的场景如果你的图主要是给非技术人员看的用户调研、业务汇报、售前材料那么图表美观度的重要性远高于可维护性。这种场景还是用画图软件精修更合适。判断标准其实只有一个这张图会不会被频繁修改如果会请用代码维护如果是一个只交付一次的结果物那怎么画都行。2. 工具选型实测Mermaid、D2、Graphviz、PlantUML 到底怎么挑2.1 四款主流工具横向对比市面上的 diagram-design 工具多得眼花缭乱但真正在实践中被广泛验证的其实就那么几款。我从语法友好度、图表类型覆盖、生态成熟度、中文支持、运维成本五个维度做了实测对比结果如下。工具语法友好度图表类型生态成熟度中文支持适合场景Mermaid高接近自然语言流程图、时序图、甘特图、状态图、类图、饼图等极高被各类文档平台原生支持不错但默认字体需要调快速画流程、时序、状态流转D2中高声明式结构清晰架构图、网络拓扑、ER 图、流程图中等社区活跃但平台集成少很好默认内置中文友好的字体配置复杂系统架构图GraphvizDOT中老牌语法较繁琐有向图、无向图、分组聚类极高很多工具的底层引擎一般需要指定字体自动化布局的复杂图PlantUML中高专门面向 UML类图、用例图、时序图、活动图高适合 UML 建模不错偏软件设计场景的 UMLMermaid 是当前生态最成熟的选手。它的语法最接近自然语言画流程图的核心就是一个箭头符号学习成本极低。而且它被 GitHub、GitLab、语雀、飞书等众多平台原生支持写进 Markdown 文档里就能自动渲染非常适合做技术文档里的内嵌图表。D2 是后起之秀主打现代架构图。它和 Mermaid 最大的区别在于对复杂布局的支持更好语法上更像在描述“盒子套盒子”特别适合表达服务间的依赖关系、网络边界、集群内部结构。D2 有几个从 Mermaid 迁移过来的人普遍觉得香的点默认的布局引擎排版很舒服文本自动换行不重叠连接线几乎不会乱穿节点。Graphviz 是祖师爷级的工具所有奇怪的图都能画但语法老旧上手曲率陡峭。我在做节点特别多、连接关系特别复杂的图谱时才会用它因为它自动布局算法真的很强可以把很乱的网状结构整理成比较清晰的层级。PlantUML 则更专精于 UML 建模。如果你日常工作是画类图、用例图、组件图PlantUML 的语法体验比 Mermaid 更规范和软件开发流程的契合度更高。2.2 我的选择逻辑按图类型分派工具没有绝对的好坏只有是否合适。我在实际项目里不是只选一个工具而是按图的类型分派让每个工具去处理它最擅长的场景。流程图、状态图、需求图中的流程步骤优先用 Mermaid。原因很现实它和文档系统结合最紧密写 Markdown 时顺手就画了不需要额外的构建环节。团队里的后端同学几乎不用学就会。比如给一个需求写“用户下单后系统走支付、回调、发货三步”Mermaid 的 flowchart 一段代码就能表达放到代码评审里也能看懂。架构图、部署拓扑图、系统模块依赖图我会用 D2。这些图通常节点多、层次多既要表达“外部客户端进到网关”又要表达“网关路由到多个微服务”还要表达“微服务之间互相调用”用 Mermaid 画很容易因为文本换行问题导致图形混乱而 D2 的布局引擎处理这种场景游刃有余。类图、用例图这类偏软件设计的图我用 PlantUML。它的语法比 Mermaid 更贴近 UML 规范类之间的关系、继承、接口实现等都有专门的语法描述生成的图也更符合设计文档的标准。Graphviz 我只在特殊场景用比如依赖关系图谱、知识图谱、网络拓扑这种节点数和边数都非常多的场景。它的稳定性是几个工具里最好的但也因为在布局上太强调自动化手动的细节调整反而比较受限。2.3 当前推荐组合与模板如果你不想折腾直接照搬我现在的组合就行Mermaid 处理文档内嵌图表D2 处理复杂架构图Excalidraw 负责草图阶段Graphviz 作为特殊场景备选。对于一个新的项目仓库我会在 docs 目录下建一个 diagrams 文件夹里面放.mmd后缀的 Mermaid 源文件、.d2后缀的 D2 源文件并在该目录下放一个 README说明每张图对应什么内容、如何导出。目录结构大概长这样docs/ ├── README.md └── diagrams/ ├── README.md ├── order-flow.mmd ├── checkout-flow.mmd ├── system-architecture.d2 └── MakefileMakefile 的好处是团队成员不用记命令行输入make diagrams就会重新导出所有图片到指定的图片目录。这样图源文件和导出图片分开管理图片只作为最终产物源文件才是真正的维护对象。3. 从零搭建一个可复用的 diagram-design 工作流3.1 项目目录与文件规范我建议所有 diagram-design 的源文件统一放在一个目录里而不是散落在各个文档目录下。集中管理的最大好处是查找和维护都方便。模块一多图表文件很容易丢失或重复统一目录配合 README 索引能避免这个问题。目录内部可以按主题再细分。比如做微服务系统的可以分成business-flow、system-arch、sequence三个子目录。命名上我习惯用短横线分隔的英文order-flow.mmd、payment-sequence.mmd、deploy-topology.d2。文件命名最好能直接表达“这张图画的是什么”时间久了不会看文件名一头雾水。还需要在 README 里写清楚两条规则第一官方源文件以文档目录下的源文件为准导出的图片不允许手动编辑第二任何图表修改都必须同时更新源文件和重新导出图片。这两条是团队协作里的底线否则又会回到截图改图的混乱状态。3.2 动手画一张支付流程图Mermaid 实战以最常见的业务场景为例我带你走一遍用 Mermaid 画支付流程图的完整过程。先描述需求用户在小程序里下单点击支付系统先查库存再创建订单调用微信支付后台异步回调最终通知用户支付成功。在.mmd文件里写这样的源码flowchart TD A[用户下单] -- B{库存是否充足} B -- 否 -- Z[订单取消] B -- 是 -- C[创建订单] C -- D[调用支付网关] D -- E[支付页面] E -- F[等待异步回调] F -- G{回调校验} G -- 失败 -- H[标记支付失败] G -- 成功 -- I[更新订单状态] I -- J[通知用户支付成功]导出这张图的命令是npx -p mermaid-js/mermaid-cli mmdc -i order-flow.mmd -o order-flow.svg -w 1280这一步会用到 mermaid-cli它本质上是把 Mermaid 语法放到无头浏览器里渲染再导出为图片。参数里的-w 1280是设置输出图片宽度。实测下来导出 SVG 比 PNG 更推荐因为 SVG 放大不模糊文档网站上引用也不占太大存储。Mermaid 语法有几个高频知识点。flowchart TD表示从上到下布局LR则是从左到右。节点后面跟{ }表示菱形判断圆括号( )是圆角矩形尖括号 代表输入输出节点。判断分支的分支文字写在连接线上-- 否 --表示一条标记了“否”的连线。3.3 用 D2 画微服务架构图D2 实战D2 适合画节点多、层次多的架构图。我画微服务架构图的体验是以前用 Mermaid 画这种图节点一多就得靠子图分组文本一长就挤成一团D2 则将“容器”、“组件”的概念自然融合进语法里每个服务可以是一个container容器里面再拆出更小的模块。用 D2 画一个基础微服务架构图源文件长这样direction: right 客户端: Client { shape: rectangle style.margin: 8 } 网关: API Gateway 认证中心: Auth Center 订单服务: Order Service { 订单接口: API 订单处理: Handler 数据访问: DAO } 支付服务: Payment Service { 支付接口: API 对账任务: Recon Task } 数据库: MySQL { shape: cylinder } 客户端 - 网关 网关 - 订单服务: 路由 /order 网关 - 支付服务: 路由 /pay 网关 - 认证中心: 校验 token 订单服务 - 数据库: 读写订单表 支付服务 - 数据库: 读写流水表 订单服务 - 支付服务: 发起支付导出命令d2 --theme 300 --layout elk system-architecture.d2 system-architecture.svg这里--theme 300是选一个更柔和的配色主题--layout elk是指定用 ELK 布局引擎。D2 默认的布局引擎叫 dagre画层次分明的图不错但遇到环路或者密集连接时ELK 对边的处理更优雅节点位置更均匀。如果你发现一张图的连线交叉很多换--layout elk通常能改善一个档次。D2 的容器语法非常直观花括号{ }内的部分会自动被渲染为容器容器的类型靠shape属性指定。shape: cylinder代表数据库shape: cloud代表外部系统。连接线与 Mermaid 的思路一致用-表示方向连线上加冒号就可以写说明文字。3.4 批量导出与 CI 自动化图多了以后一条条执行导出命令会浪费大量时间而且容易遗漏。我在项目里会用 Makefile 管理这些命令。figs docs/images diagrams: npx -p mermaid-js/mermaid-cli mmdc -i docs/diagrams/order-flow.mmd -o $(figs)/order-flow.svg d2 --theme 300 --layout elk docs/diagrams/system-architecture.d2 $(figs)/system-architecture.svg echo All diagrams exported. .PHONY: diagrams这样团队成员或者 CI 只需要执行一条make diagrams就能把整个 docs 目录下的所有图全部重新导出。我还会加上一条规则如果源文件比图片文件新就自动重新导出。用find做增量判断避免每次都全量生成。更进一步如果你的文档系统构建流程已经在 CI 里跑完全可以把图表导出步骤也挂进去。比如在流水线里加一个 stage每次有新 commit 就重新生成全部图片并把变更后的图片提交回仓库。别人看到的效果就是源文件一改文档里的图跟着自动更新根本不需要人工干预。4. 实战中踩过的坑排版、乱码与团队协作4.1 中文乱码问题用代码画图最常遇到的问题就是中文乱码。Mermaid 默认渲染字体对中文支持不好导出 PNG 时可能出现方框字或者乱码D2 对中文的支持稍好但不同版本在不同系统上表现也不一样。Mermaid 的解决方案是给 mermaid-cli 提供一个外部配置文件mmdc.json指定渲染时使用的字体{ fontFamily: PingFang SC, Microsoft YaHei, Noto Sans CJK SC, sans-serif, fontSize: 16 }调用命令时加上-c mmdc.json。这样中文字体就会回退到系统里可用的中文家族基本能根治乱码问题。D2 的中文乱码多发生在 Linux 服务器上因为服务器默认没有安装中文字体。最省事的办法是安装fonts-noto-cjk软件包然后运行fc-cache -f刷新字体缓存。D2 会自动使用系统里的中文字体生成出来的 SVG 中文显示就完全正常了。4.2 布局不可控问题代码画图的布局是自动计算出来的好处是省心坏处是复杂场景下一次不一定满足要求。我遇到最多的布局问题是文本过长导致节点过大或者两个节点之间连线过长视觉上不平衡。Mermaid 的解法是控制节点内的文本长度再不行就换行。Mermaid 也支持在节点里用br/标签手动换行。另一个实用技巧是把过长的判断条件简化比如“库存是否充足且商品状态为可售”这种长文案拆分到两个节点里图上可读性要好得多。D2 的布局优化有更明确的姿势。方向用direction: right或direction: down控制节点之间的间距可以通过style.stroke和style.margin调整如果某个子图内的节点总是错位可以给它单独设置style尺寸让它的内部空间更大一些。如果你遇到怎么调都调不好的极端情况还有一个终极大招导出 SVG 之后手动编辑 SVG 坐标。但这会破坏“源代码是唯一事实来源”的原则做一两次可以长期不建议。4.3 团队协作与评审代码画图的团队协作核心不是工具本身而是流程。我在实践里总结了一条经验图表应该像代码一样做评审进入仓库的每一张图都必须有对应的源文件和在文档中的引用位置。我们团队的习惯是涉及系统架构的改动必须在 MR 描述里附上调整后的架构图并且在评审中讲清楚“改动前到改动后边界在哪里移动了新增了哪些连接”。因为图通常是一眼能看到全局的它比文字描述更容易让评审者抓住变更的本质。还需要约定一个更新规则。比如“谁改动了服务谁负责在同一个 MR 里更新架构图”这个约定比任何文档都更有效。实现方式也很简单在 CI 里检查 docs 目录的图片导出时间和源文件修改时间如果源文件比图片新就报一个警告。这样把维护义务硬性绑定到提交环节就不会再出现文档和系统脱节的情况。4.4 常见问题速查表把我平时被问到最多的问题整理成一张表遇到问题可以直接对照排查。问题可能原因解决方案导出的图片中文乱码系统缺少中文字体在配置文件中指定中文字体Linux 安装 noto-cjk 字体Mermaid 节点文本重叠节点文字过长或未手动换行缩短文本使用br/换行或缩窄节点内容D2 连线交叉过多布局引擎不适合当前图结构切换--layout elk或调整direction方向图片尺寸过大图中节点过多输出分辨率高导出时降低宽度参数或使用 SVG 格式导出命令依赖 Node/npm本机未安装 mermaid-cli使用 npx 临时调用或配置好 package.json 脚本团队成员忘记更新图缺少自动化检查在 CI 里比较源文件和图片文件时间戳5. 把图设计得“能看”的经验5.1 一张图只讲一件事我见过很多人画架构图喜欢把所有东西都塞进去从浏览器到数据库从消息队列到监控系统恨不得一整张图把整个公司 IT 系统都画完。结果就是没有人能看明白图成了摆设。diagram-design 虽然解决的是代码层面的问题但图的核心价值永远是沟通。一张好的架构图应该能在五秒内被人读懂我遵循“一张图只讲一件事”的原则讲部署拓扑就只画服务器和中间件不画业务调用关系讲业务时序就只画消息和接口调用不画物理设备。如果系统本身很复杂我倾向于画一组图分为全局架构图和局部详情图。全局图体现系统与外部系统的边界局部图深入某一个子系统内部。读者需要哪个层面的信息就去哪张图里找。5.2 分层不要一张图装下整个世界架构设计里有一个经典的 C4 模型把架构图分为上下文图、容器图、组件图和代码图四个层次。diagram-design 实践里我非常推荐借用这个思路因为它天然匹配“一张图只讲一件事”的原则。上下文图描述系统与外部用户和外部系统的关系整张图通常只有三到四个区块非常简洁。容器图描述系统内部的应用服务、数据库、消息队列这些可部署单元这是绝大多数架构评审用的层级。组件图再往下拆对每个容器里的模块进行细化。我当前项目里系统架构相关的图就严格按这三层维护。上下文的context.d2、容器的container.d2、组件级的component-order.d2。分层之后每张图都不会太大维护成本低评审时也能准确定位讨论范围。5.3 样式和色彩的克制代码画图天生容易在样式上翻车因为默认主题可能不够精致而手动调样式又非常耗时。我的经验是样式宁少勿多颜色尽量控制在三到四种。以 D2 为例我会给不同层级的元素分配统一的颜色语义外部系统用灰色自己的系统用品牌蓝色数据库用绿色失败或异常链路用红色。这套规则要在团队内统一避免每个人画出来的图各带一套配色。定义好之后在源文件里为容器设置style.fill和style.stroke同一张图内保持一致即可。还有一个细节是箭头的方向性。所有连线尽量保持同一种方向约定比如自上而下代表调用自上而下传播左进右出代表数据流向从左到右。否则看图的人容易误解依赖关系。5.4 源码里写注释diagram-design 既然把图当作代码维护那么写注释就是一个自然延伸。很多人写图源码的时候不肯写注释觉得“图本身就能说明问题”但时间一长当初为什么这么画、这条边表达的深层含义是什么没有人记得住。我在 D2 和 Mermaid 源文件里都会加注释。Mermaid 用%%开头写注释D2 用#开头。注释里写清楚三件事这张图存在的目的是什么最近一次修改是因为什么业务变化是否有需要特别注意的限制条件。例如# 该架构图对应订单域 V3 版本 # 2025 年初拆分支付相关能力订单服务不再直连支付渠道 # 注意部分遗留环境仍走旧链路图中以虚线标记这样其他同事接手你的图时看到的是“有人维护过的产物”而不是一段需要猜谜的代码。表格、流程图、架构图最终服务于团队的认知一致性维护成本的降低会直接反映在协作效率上。我在实际维护 diagram-design 之后的体会是真正让我觉得这笔投入值回票价的不是省掉了多少拖拽时间而是团队对“系统到底长什么样”这件事终于有了一个准确且可信的依据。每张图都有明确责任人每次变更都有迹可循评审时大家看的是同一个版本这些隐性收益远远大于画图工具本身带来的效率提升。如果你正在被文档图反复更新折磨我的建议很直接挑一个小范围的图比如一张流程状态图试着用 Mermaid 或 D2 重画一遍放进仓库跑一次之后你自然会感受到差别。