拓冰建站拓冰建站
首页 / 资讯中心 / 正文

图表即代码:用D2与Graphviz实现架构图的版本化与自动化管理

架构图画到最后基本都是一个结局新版在旧版上改旧版又被另一个人存了一份十个人手里有十一个版本。我在维护一套内部系统的技术文档时被这个问题折磨得不轻后来索性把“diagram-design”当成一个正经工程项目来做——用代码定义、生成和管理所有技术图表。这个项目解决的就是我前面说的那类问题图表无法版本化、改一版崩一遍、风格全靠手气。它的核心思路是“图表即代码”一个文本文件进去SVG/PNG出来所有中间产物都能被 Git 追踪、被 Code Review 审查。如果你也负责架构文档、技术方案或者系统说明书的长期维护这篇文章会给你一条省心的路子。1. 混乱从哪来手工画图的四个死穴以及 diagram-design 的解法先说清楚这个项目不是哪个商业软件的替代品我做的是一套围绕“diagram-design”这个理念的工作流。所谓 diagram-design说白了就是别把画图当一次性表达而是当成持续演进的设计对象。工程上对待代码怎么较真对待图表也该怎么较真。1.1 手工绘图工具的真实痛点我之前也用过很长一段时间的拖拽式画图工具比如 draw.io、ProcessOn 这类。单张图、几个人协作、画完就发群里确实快。但一旦图表进入“长期维护”状态问题就全冒出来了。第一是改图比画图难十倍。节点一多连线绕来绕去想加一个模块就得手动挪位置挪完一个节点相邻的线全乱。这种时间成本不像写代码——代码改了运行不对至少报错很明确图改了布局乱掉只能靠肉眼一点点调。第二是扩散性太强。一个系统架构图经常被人截图放到需求文档里再被另一个人放到周报里然后每一份都过期了。你永远不知道哪张图是最新的。更麻烦的是想追溯“这张图上一版是什么样”几乎不可能。第三是不可审查。团队里如果有同事在 CR 里指出了一个架构问题这本身是好事但如果牵涉到图表大家只能“你发个新版吧”没人能清楚地指出新旧之间到底改了什么。图的变更没有 diff就没有真正意义上的 review。第四是风格撕裂。张三画的图标风格、配色、线型李四完全不管画出来像是两个团队的成果。时间一长文档里的图既不专业也不好懂。1.2 diagram-design 给出的解法把图当代码管我后来把整套思路收敛成一个叫“diagram-design”的项目核心其实只有三条约束一切图表以文本方式声明。无论是架构图、流程图还是时序图全部用可阅读的文本源文件定义不依赖任何私有格式。同一套源码只产出一类产物。每种图都固定输出 SVG 和 PNG 两种格式不允许手改产物。变更必须走版本库。所有图表源文件进入 Git 仓库改图必须提交提交必须能看出改了什么。这三条约束落下来之后图表就从“一次性消耗品”变成了“工程资产”。我能把图表放到代码评审里能在历史提交里找出版本演进甚至能用脚本批量检查所有图的合法性和引用关系。2. 选型实录D2、Graphviz、PlantUML、draw.io 的对比与取舍定下“图表即代码”的方向后摆在我面前的是工具选型。市面上不少工具都能做到“文本生成图表”但我实际试下来差别比想象中大。我做了个简单对比表先把结论放出来工具语言学习成本布局质量中文支持复杂图表现维护状态Graphviz(DOT)低强细粒度可控需配置字体强适合大图稳定社区成熟D2低强自动布局省心内置支持好适合中小型图快速迭代社区活跃PlantUML中一般样式古早一般适合时序/用例老牌更新较慢draw.io无需学习手动布局漂亮好强但非文本优先2.1 我给自己列的四个硬指标我选型时没有只盯着“能不能生成图”这个基础条件毕竟能生成图的工具太多了。我给自己的项目定了四条硬指标文本化程度、布局质量、中文兼容、复杂图的可维护性。文本化程度指的是源文件能不能在代码库里很好地做 diff。有些工具虽然也是文本但语法冗长改一个节点要连带改七八处那就不算合格。布局质量更重要我要的是“机器生成的图也能见人”而不是还要手工拖半天。中文兼容很容易被忽略很多开源工具默认字体不支持中文我踩过好几次。可维护性则体现在大图上一个项目有四十个节点工具能不能帮我保持局部清晰这很关键。2.2 我为什么最终定了 D2 Graphviz 的组合单独用哪个都会有点遗憾。Graphviz 很能打控制力极强适合复杂流程和集群但 DOT 语法写起来偏底装饰性功能弱想画得有现代感得费不少功夫。D2 则刚好相反语法极其简洁写起来像在描述关系默认样式也长在当代审美上特别适合快速出漂亮的架构图。但 D2 目前的生态相对年轻某些极端复杂布局还不如 Graphviz 灵活。所以我最后没有二选一而是建立了一个兼容策略常规技术架构图、网络拓扑图用 D2因为它在分层和分组表达上很自然复杂流程图、状态机图用 Graphviz因为它对 cluster、rank、edge 约束的控制力更强。两个工具都用文本源文件统一纳入项目目录互不干扰。这里补充一个我个人的实操习惯不追求一个工具吃遍所有场景。diagram-design 的重点是“设计流程的统一”而不是“工具的统一”。只要所有图表源码都进 Git、都按脚本导出底层工具是谁根本不重要甚至可以随时替换。2.3 项目目录与产物规范选型定了之后整个项目结构就很清晰了。我按“源码、配置、产物”三层来组织diagram-design/ ├── diagrams/ # 所有图表源码 │ ├── d2/ # D2 源码 │ └── graphviz/ # DOT 源码 ├── styles/ # 共享样式/模板变量 ├── scripts/ # 导出、检查、发布脚本 ├── dist/ # 生成的 SVG/PNG不允许手改 └── docs/ # 图表索引和使用说明产物目录被 Git 忽略的只有临时文件导出文件我会强制提交因为文档站和团队协作用到的其实是最终产物。源码和产物同时入库别人拿到的永远是一套完整自洽的东西。3. 可复用的图表设计骨架目录、模板与变量分层选完工具只是第一步。真正让“diagram-design”这个项目运转起来的是它内部那套可复用的设计骨架。如果没有这层抽象每次画图都会退化成“重新发明一次样式”。3.1 目录结构背后承载的分层思想在前面那个目录树里diagrams/、styles/、scripts/其实分别对应三种关注点内容、样式、动作。diagrams/里放的是图表的“内容层”就是“有哪些节点、谁连谁”。styles/里放的是“表达层”即“节点长什么样、连线什么颜色、字体多大”。scripts/里的脚本则承担“动作层”负责把内容和表达组合起来产出最终图片。这种分层在设计代码时是常识做图反而容易被忽略。但如果没有这层抽象你很快就会发现自己在一遍遍地复制样式代码改一个品牌色要翻遍所有图。把变量和模板抽出来之后改主题这件事就变成了改一个变量文件的事。3.2 模板变量与 D2 的联动我举一个在项目里实际能跑通的例子。styles/下放一个theme.d2文件vars: { colorPrimary: #2F5AF3 colorOk: #16A34A colorWarn: #D97706 colorMuted: #94A3B8 fontFamily: PingFang SC } vars: { nodeStyle: { shape: rectangle stroke: 4 stroke-width: 4 fill: #F8FAFC stroke: ${vars.colorPrimary} font-size: 14 font-family: ${vars.fontFamily} } }然后在我的架构图源码里引入这个主题...this imports the theme vars: { sourceTheme: ../styles/theme.d2 sourceVar: vars }实际用到的 D2 版本不同include 语法会有差别但思路是一致的所有视觉 token 集中在一个文件。改主题色全项目图表全部同步更新。这样才有资格叫“设计系统”否则只能叫“一堆图”。3.3 把“画图习惯”固化成脚本有了主题之后我还做了一件很关键的事把“怎么导出图”的琐碎习惯写成脚本。以前的流程是打开工具、点导出、选格式、选路径每次少说十几次点击。脚本化之后一行命令搞定。#!/usr/bin/env bash # scripts/export.sh # 导出 project1 架构图 d2 diagrams/d2/project1.d2 dist/d2/project1.svg # 同时转一份 png用于文档站展示 d2 --layout elk diagrams/d2/project1.d2 dist/d2/project1.png这个脚本本身没什么技术含量但它保证了所有人操作一致。团队里新同学想加一张图照着模板建文件、跑脚本产出的图跟老图风格完全统一。这一步是把个人经验和习惯变成团队基础设施的关键。4. 核心图型实战架构图、流程图、时序图的代码化方法骨架搭好之后我们来看实际内容。我在这个项目里覆盖了三种最常见的图型系统架构图、复杂流程图、时序图。每一类都有自己的表达习惯和注意点。4.1 系统架构图用 D2 表达分层与依赖D2 表达分层架构非常自然因为它天生支持嵌套。我在diagrams/d2/order-system.d2里写了一个订单系统的简化示意api-gateway: API Gateway services: { order: 订单服务 payment: 支付服务 inventory: 库存服务 } data: { order-db: 订单库 inventory-db: 库存库 } api-gateway - services.order services.order - services.payment services.order - services.inventory services.payment - data.order-db services.inventory - data.inventory-db这段源码的可读性在于结构就是图的形状。读者不需要运行工具光看缩进和箭头就能知道系统由哪几层组成、依赖方向是什么。实际项目里还要补上更多细节比如协议标注、密钥信息、流量方向。我在 D2 里会用label或near对象来做说明。这里有个经验箭头上的文字别超过一行如果状态流转需要写大段说明把它放到节点内部或者单独开一页设计说明图比挤在一张图里健康得多。4.2 复杂流程图Graphviz 的 cluster 与 rankD2 在 20 个节点以内的架构图上表现优秀一旦上到复杂的业务流程图我通常会切到 Graphviz。DOT 语法虽然啰嗦但在“控制布局”这件事上有绝对话语权。举一个支付对账流程的例子digraph reconciliation { rankdirTB; node [shapebox, stylerounded, fontnamePingFang SC]; subgraph cluster_input { label数据准备; a1 [label拉取交易流水]; a2 [label拉取支付渠道对账文件]; } subgraph cluster_core { label核心对账; b1 [label按订单号关联]; b2 [label比对金额与状态]; b3 [label差异记录]; } subgraph cluster_output { label输出; c1 [label生成差异报表]; c2 [label推送告警]; } a1 - b1; a2 - b1; b1 - b2; b2 - b3; b3 - c1; b3 - c2; }这里两招很关键cluster用来给节点分组视觉上会自带一个框rankdir控制整体方向。真正复杂的图不是节点多而是关系乱所以我会不厌其烦地把大图拆成 cluster让每个 cluster 内部尽量保持高内聚跨 cluster 的连线越少越好。4.3 时序图的边界什么时候不适合代码化我在项目里也尝试过用 PlantUML 画时序图一开始觉得很新鲜几行就能画一条消息线。但用久了发现时序图是几类图里“收益相对小”的。因为时序图阅读时眼睛关注的是纵向的时间轴和消息先后它更适合在需求讨论阶段快速画草图一旦进入长期维护反而容易因为频繁增减消息而导致源码变成一团乱麻。我在这个项目里保留较小规模的时序图就是为了快速展示交互顺序但详细的协议字段说明我会放到 Markdown 文档里用表格写清楚每一步的参数和异常分支。所以这里我给出的建议是不是所有图都值得代码化。diagram-design 的前提是长期维护和多人协作如果一张图画完就完那拖拽工具依然是效率最高的。时序图如果场景变化快先用手画等到它稳定了再沉淀成源码。5. 把图表变成工程资产版本化、审查、自动导出这一部分才是 diagram-design 最值钱的地方。如果只是把画图工具从桌面软件换成了代码那意义有限。真正的工程化在于图表能不能被版本管理能不能自动生成能不能通过 CI 检查。5.1 在 Git 里审查图表变更图表的源码进了 Git 之后“看图”这个动作就变成了“看 diff”。D2 源码对比一下能一眼看出新增了哪个节点、删了哪条连线。我以前在审查里遇到的典型场景是系统架构调整服务 A 不再直接调用服务 B而是通过消息队列 C 解耦。放在传统画图工具里这个变更要从截图里肉眼找。但在代码源文件里diff 非常清晰services.order - services.payment - services.order - services.inventory services.order - mq.publish mq.subscribe - services.inventory这种审查体验让团队成员愿意主动在 PR 里包含图表变更因为每个人都看得懂。以前改图最怕的是“我也不知道自己动了啥”现在 Git 告诉我动得明明白白。5.2 用 CI 自动导出并校验手动跑导出脚本还不够真正的工程化需要自动化。我在.github/workflows/下加了一个 workflow核心步骤很简单name: export-diagrams on: push: paths: - diagrams/** - styles/** jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Install D2 run: curl -fsSL https://d2lang.com/install.sh | sh -s -- - name: Install Graphviz run: sudo apt-get install -y graphviz - name: Export run: bash scripts/export-all.sh - name: Upload artifacts uses: actions/upload-artifactv4 with: name: diagram-dist path: dist/这个流程保证每次图表源代码变更都会重新生成一份最新产物并作为构建产物留存。如果团队配了文档站还可以把导出的 SVG 文件自动同步过去省去所有手工上传操作。我还在 scripts 里写了一个“引用检查”脚本它会扫描所有源码检查声明的节点是否被使用、是否有孤立节点、是否引用了不存在的样式变量。这个脚本不追求百分百智能它更像一个 linter能拦住低级的“无意义箭头”和“重复节点”。5.3 图表的引用与文档联动图表画得再好如果不能被文档直接引用价值也要打折扣。我选择了最朴素的做法Markdown 引用 SVG 文件而不是截图。![订单系统架构图](../dist/d2/order-system.svg)这个做法的好处是当 dist 目录被 CI 更新后文档站重新构建新图自动上线。源文件和文档彻底解耦文档维护者不需要关心图是怎么生成的只需要保证引用路径正确即可。这个方案我在多人协作的文档仓库里验证过最直接的感受是文档里再也没有“过期截图”了。6. 踩坑实录与经验沉淀流程跑通不代表没有坑。我在 diagrat-design 落地过程中踩了不少挑三个最典型的说说。6.1 中文字体导致的乱码与豆腐块我最初用 Graphviz 导出 PNG 时中文全部变成一个个方框。典型症状是 SVG 里没问题但转 PNG 就乱。排查后发现是系统缺少可用的中文字体Graphviz 按照字体名找不到对应字体就回退成了一个不存在的家族。解决办法分两步第一步安装字体在 macOS 上我用的是 PingFang在 Linux CI 上我安装了 Noto Sans CJK SC第二步在 DOT 源文件里显式声明fontname不要依赖环境默认值。D2 对中文的默认支持会好一些但仍然建议在主题变量里固定font-family避免在别人的机器上效果不一。6.2 布局“失控”后的三板斧D2 和 Graphviz 有时会生成让人摸不着头脑的布局比如本应相邻的节点隔了十万八千里或者一条线横穿了整个图。遇到这种情况我的第一反应不是去微调坐标而是检查“图的设计”是不是出了问题。我总结了一套叫“三板斧”的排查顺序拆 cluster把高内聚的几个节点包进同一个 cluster减少全局节点数。显式排序在 DOT 里使用rank在 D2 里尽量按阅读顺序写代码块。减少跨层连线如果发现大量连线从第一层直接穿到第三层说明分层粒度不对该在中间补一层网关或适配器。按这三板斧走绝大多数布局乱象不是布局的锅而是建模粒度出错了。6.3 一个 40 节点大图给我的教训我做了一个把所有服务、中间件、依赖关系全塞进一张图的项目示意图40 多个节点20 多条交叉线。导出来后我自己都看不懂更别说读者。后来我把这张图拆成了 4 张总览图只展示服务分组和主要依赖分组 A 的细节图分组 B 的细节图以及一张部署拓扑图。总览图保持在 15 个节点左右每张细节图控制在 10 个节点以内。拆完之后图片的可读性立刻上来了。这个教训让我定下一条项目规则任何一张图节点数超过 25 就必须考虑拆分。这条规则后来被写进了scripts/check-size.sh作为 CI 检查项。超过阈值直接报警提醒作者重新考虑图的设计。我在实际维护中对“diagram-design”这套工作流最深的感受是它不会让你画第一张图变得更快但会让你的第 50 次修改、第 100 次回溯变得极其轻松。如果你手头正好有一堆长期没人敢动的架构图不如挑一张最头疼的用 D2 或 Graphviz 重写一遍把它放进 Git跑一次自动导出。等你在 PR 里看到清晰的图表 diff 时大概就明白我为什么说回不去手工画图了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门