ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

代码驱动图表:把架构图工程化,纳入版本控制与自动化流水线

2026/9/9 4:40:02 拓冰建站 浏览量
代码驱动图表:把架构图工程化,纳入版本控制与自动化流水线 我做架构设计这些年最怕的不是写代码而是画图。不是那种随手画给同事看的意思是正经交付给团队、写进设计文档、贴在 Wiki 里、后续三个月还得继续维护的架构图。你改了一版服务拆分就得同步改图改完还得检查有没有漏连线、有没有图层重叠、有没有版本对不上。早期用 Visio、后面用 draw.io折腾了好几年问题始终没根治。后来我把图表当代码来管理建了一套叫 diagram-design 的工程化方案用代码驱动图表生成把架构图、流程图、时序图、数据流图全部纳入版本控制整个团队从维护一团乱麻变为维护一套活文档这件事才算真正落地。这篇就讲讲这套方案的设计思路、落地细节和途中踩过的坑。整套方案用一句话概括图表不是画出来的是用代码声明出来的再通过自动化流水线渲染成多人可协作的产物。这里不限于某一种特定工具核心是把图表当成一等公民纳入研发流程用工程手段让图表可追踪、可评审、可回归、可复用。适合从后端框架设计到前端模块拆解、从数据库关系梳理到业务流程抽象的所有场景只要你的团队还在为文档里的图过期了这件事头疼都可以参考这套做法。1. 为什么我不能再用画布工具了图表工程化的底层逻辑先说一个很奇妙的观察。代码可以 git diff、可以 code review、可以追溯历史但传统画布工具生成的图做不到。一张成型的 PNG 在仓库里躺了一个月没人知道它画的到底是哪一版架构。你去问原作者他可能自己也说不清。这个痛点很多人都有但一直没被认真对待因为大家都觉得画图本来就是一次性工作。实际上不是。任何一张有价值的图都和它描述的系统一样处于持续演进状态。系统从单体拆成微服务、从同步调用改成消息驱动、从一个数据库拆成读写分离图上的每一次变更都应该被记录、审视、复盘。画布工具天然做不到这件事因为它的产物是二进制或私有格式不是文本diff 无从谈起review 也无从谈起。1.1 图形化工具的三个死穴用画布工具画图有三个绕不开的问题我用实际经历来说。第一是协作割裂。设计评审的时候大家指着腾讯会议共享屏幕说这里应该加一条虚线然后一切修改只发生在某一个人的本地文件里。没有留痕没有记录过两周再打开那张图谁改过、为什么改、什么时候改的全部遗失。第二是过期惰性。架构图最怕的不是丑而是假。图上画着六个服务实际上线上只有四个还有两个是三个月前的新模块图里根本没体现。维护成本越高惰性越强最后所有人默认文档里的图仅供参考这张图就彻底失去价值了。第三是无法复用。每次画图都是从空白画布开始拖一个框、填一段文字、连线、调颜色。工作量大不说风格还很难统一。团队五个人画出来的图放一起视觉语言完全不在一个频道上阅读者光适应风格就很费劲。1.2 代码驱动图表的分层逻辑代码驱动图表的本质是把内容和表现分离。内容就是图的语义有哪些节点、节点之间什么关系、数据怎么流动。表现则是渲染层的事节点摆哪里、颜色用什么、连线怎么路由。传统画布工具把这两层揉在一起你拖拽的时候既要管内容又要管排版。代码驱动则让你只写内容渲染交给工具。这套分层逻辑和 Web 开发里的 HTML/CSS 分离其实是同一套思想。你只管语义结构不管视觉表现工具用一套既定规则帮你完成布局。输出的结果可能不如手工调整那么精致但换来的是内容可维护、风格可统一、结构可评审。对于技术文档场景这两个特性远比一张精美到像素级的图重要。1.3 工具选型的取舍模型市面上代码驱动图表工具不少我挑主流的三个做了深度对比分别是 Mermaid、PlantUML 和 D2。维度MermaidPlantUMLD2上手成本很低类 Markdown 写法中等语法接近代码中等偏低声明式语法生态集成GitHub/GitLab 原生渲染文档平台支持广老牌兼容工具多各类插件丰富较新CI 集成好文本定位强布局引擎自动布局复杂图稍乱自动布局成熟方向控制灵活布局质量高自动分组能力强时序图支持很强特别适合异步交互也很强类代码建模风格一般主打关系图扩展性一般主题定制有限一般但有多种 skinparam强变量/模板/主题分离我的结论是没有最强的工具只有最合适的场景。团队如果之前完全没接触过代码画图从 Mermaid 入手最平滑GitHub 直接渲染零额外成本。如果画大量时序图且希望表达精确PlantUML 的建模能力更有优势。如果系统复杂度高、节点多、关系密集D2 的布局质量和文本定位能力会帮你省很多事。diagram-design 这套方案并没有锁死某一个工具而是在上层做了一层适配。每个图的源文件声明自己用什么语言渲染底层统一走构建管线。这样团队可以按图选型不用被单一工具限制。2. diagram-design 的基建与约定目录、命名与版本流选完工具只是开始真正让这套方案在团队存活下来的是一套好的工程默认值。没有约定每个人按自己的想法建目录、起文件名、写语法三个月后仓库照样是一锅粥。所以我花了很多精力在基建层先把规矩立起来。2.1 目录结构与命名规范diagram-design 采用按图类型划分顶层目录、按业务域划分子目录的结构。这样做的好处是你想找某张图的时候先确定它是哪种类型再确定它属于哪个业务域两条路径一锁就定位了。diagram-design/ ├── architecture/ # 系统架构图 │ ├── payment/ # 支付域 │ ├── order/ # 订单域 │ └── user/ # 用户域 ├── sequence/ # 时序图 │ ├── payment/ │ └── order/ ├── flowchart/ # 业务流程图 │ └── refund/ ├── er/ # 实体关系图 │ └── order-center/ ├── common/ # 共享主题、通用片段 │ ├── theme.d2 │ └── snippets/ ├── scripts/ # 构建与校验脚本 └── .diagramlint/ # 自定义校验规则命名规范制定了一条强制规则所有文件名必须用 kebab-case且要包含业务域和内容描述。比如payment-refund-flow.d2、order-create-sequence.puml、user-center-arch.mmd。禁止出现未命名.d2、新建文档.mmd这种名字。文件名就是图的索引起得清楚后面检索才高效。这条规范我用一个小小的 pre-commit hook 来强制检查文件名不合法直接拦截提交。团队从反感到习惯大约只用了两周因为规则足够简单几乎没有学习成本。2.2 主题与样式的全局约定代码驱动图表最大的视觉优势就是主题统一。我把颜色、字体、间距、图标风格全部收敛到全局主题文件里各图引用同一份配置。这样无论哪个团队画的图渲染出来视觉风格是同一套语言。以 D2 为例我在common/theme.d2里统一了颜色语义vars: { d2-config: { theme-id: 200 layout-engine: elk pad: 64 } } # 颜色语义主色、辅助色、告警色 vars: { color-primary: #2D6AFF color-secondary: #6C8EAD color-warning: #E6A23C color-danger: #F56C6C color-border: #D9DEE8 color-bg: #F7F9FC }这里颜色不能随便配我定的是按语义用色主链路用主色旁路/备选链路用辅助色告警/失败路径用告警色依赖容器背景用浅底色。这样一张图扫过去读者第一眼就能分清主次而不是被花花绿绿的颜色干扰。Mermaid 也有类似的 theme 配置可以在%%{init: {theme: base, themeVariables: {...}}}%%中声明。PlantUML 则是通过!theme指令引用。三套工具的配置语法不一样但语义是同一个全局统一、按语义用色、禁止局部自定义。2.3 图表管理的最小版本流程这一节是整套方案的灵魂。代码驱动图表一旦纳入 git就等于天然具备了版本能力。但有版本和做得对是两回事我总结了三个关键动作。第一个动作是提交信息规范化。图表文件的提交信息要写清楚改了什么、为什么改。比如docs(diagram): 在退款时序图中新增超时补偿分支。这不是形式主义是让历史可读。三个月后想查退款链路什么时候加的超时分支一句 git log 就定位了不需要打开文件慢慢比对。第二个动作是评审拉上业务方。图的变更关联的是业务流程变更所以评审不能只看画得对不对还要看流程对不对。我在 PR 模板里加了一个diagram-check区块提交图表变更时必须勾选是否更新了关联流程图、是否走查了异常分支、是否同步了关联文档链接。这个动作让图表变更从视觉修改升级为业务变更评审维度立刻不一样了。第三个动作是打 tag 归档关键节点。每逢大版本发布把对应的架构图快照打一个 tag例如arch-v2.3.1。这个 tag 记录的是一段时间内系统架构的真实状态后面做架构演进对比、做新同事培训都是现成的素材。比截图放在共享文件夹里强得多因为 tag 能精确关联到代码版本。3. 从能看到耐看三类高频图表的模板化方法代码驱动解决了能看的问题但离耐看还差一步图的内容结构要设计得好。这一步和工具无关和图谱思维有关。我以架构图、时序图、数据流图三种高频类型各展开一个模板套路这些都是可以直接抄作业的。3.1 架构分层图的模板套路分层、分组、边界架构图最常见的病是平铺。所有服务节点摊在一张大画布上大小一致颜色一致连接线密得像蜘蛛网。读者根本分不清哪个是入口哪个是依赖哪个是核心域。我的模板套路是三分法先分层再分组最后画边界。第一层是展示层/接入层第二层是应用服务层第三层是领域服务层第四层是基础依赖层数据库、缓存、消息队列。每层之间用容器分组表达核心域用醒目颜色标出非核心域用低饱和色弱化。一张架构图如果让读者三秒钟内说不出系统分几层这张图就是失败的。以 D2 为例子分层图的核心写法是嵌套容器clouds: 数据中心 { shape: cloud layer_ingress: 接入层 { api_gateway: API Gateway auth_service: 认证服务 } layer_biz: 业务层 { order_service: 订单服务 { order_core: 订单核心域 order_side: 订单辅助域 } payment_service: 支付服务 } layer_base: 基础服务 { db_primary: MySQL 主库 { shape: cylinder } cache_redis: Redis 集群 { shape: cylinder } mq_kafka: Kafka 集群 } } api_gateway - auth_service: 校验 api_gateway - order_service: 下单 order_service - payment_service: 发起支付 order_service - db_primary: 读写订单 order_service - cache_redis: 缓存订单 payment_service - mq_kafka: 发送支付结果事件注意分层的核心不在语法而在思维。接入层只做接入业务层只做业务基础层只做能力。边界画清楚了系统职责一目了然画图的过程本身就是一次架构审视。如果你在画这张图的时候发现某个服务不知道放哪一层恭喜你这通常意味着架构本身有问题。3.2 时序图的关键路径与异常分支双轨建模画时序图最大的坑是只画正常路径。拿到需求照着 happy path 画一遍就结束了。可真实的系统里超时怎么办重试几次失败后回滚什么这些才是技术评审最需要讨论的内容。我的做法是双轨建模正常路径画一张主图异常路径画一个补充片段。主图保持干净只画核心交互一般不超过 8 个参与者超过就说明这个场景粒度太大。异常路径用 loop/alt 块在图的末尾集中表达而不是把主流程画得密密麻麻。以 Mermaid 为例sequenceDiagram participant C as 客户端 participant G as 网关 participant O as 订单服务 participant P as 支付服务 C-G: 创建订单请求 G-O: 校验并落库 O-P: 发起支付 P--O: 支付受理成功 O--G: 订单状态更新 G--C: 返回下单成功 Note over O,P: 异常分支支付超时 alt 3秒未收到支付回调 O-P: 查询支付状态 P--O: 处理中 else 超过15秒 O-P: 关闭支付单 O--C: 通知支付超时 end这个模板背后的思考是评审代码不一定能发现问题但评审时序图很容易发现问题。画着画着你就会发现原来这个接口需要幂等、原来回调不保证有顺序。一套好的时序图模板本质上是一张业务风险的检查表。3.3 数据流图的极简化原则限制节点、标注方向、标记存储ER 图和数据流图是数据库设计阶段的必需品但这张图也最容易画成一张巨大的蜘蛛网。表有五十张关系有八十条全部画上去渲染出来一片黑谁都不想看。我的简化原则只有三条。第一每张数据流图只表达一个业务域最多 12 张核心表超出就拆图。第二连线必须标注方向语义比如创建更新读取不能只有一个裸箭头。第三标记存储类型区分 MySQL 表、Redis 缓存、ES 索引不能所有存储画成一个样式。用 Mermaid erDiagram 举例我会这样设计erDiagram CUSTOMER ||--o{ ORDER : 创建 ORDER ||--|{ ORDER_ITEM : 包含 ORDER }o--o{ PRODUCT : 选购 ORDER ||--o{ PAYMENT : 支付 PAYMENT }o--o{ REFUND : 发起退款 CUSTOMER { bigint id PK varchar name varchar mobile } ORDER { bigint id PK bigint customer_id FK varchar order_no int status }每张表字段只列关键字段不是把整张表的 DDL 搬上去。核心表和核心关系用实线加粗非核心关系用虚线弱化。这样数据流的骨架一眼可见。4. 把图表接入研发流水线构建、校验与产物管理方案要真正在团队里生根不能只靠自觉。人是惰性的如果画完图还要手动跑命令、手动导出图片、手动传到文档平台两三次之后大家就开始犯懒了。所以我把这块做成了自动化流水线提交代码自动构建、自动校验、自动发布全程无需人工介入。4.1 构建脚本的演进从单文件到增量编译最早的构建脚本非常简单就是一个 for 循环遍历所有.d2文件逐一渲染。图表数量少的时候还行九十张之后每次构建要一分多钟明显拖慢节奏。后来改成了增量编译只渲染 git diff 中变更过的文件秒级完成。核心逻辑不复杂就是比较时间戳和 git 状态。脚本用 shell 写比较清晰核心思路如下changed_files$(git diff --name-only HEAD~1 | grep -E \.(d2|puml|mmd)$) for file in $changed_files; do case $file in *.d2) d2 --theme $(dirname $file)/theme.d2 $file out/${file%.d2}.svg ;; *.puml) plantuml -tsvg -o out/${file%.puml} $file ;; *.mmd) npx -p mermaid-js/mermaid-cli mmdc -i $file -o out/${file%.mmd}.svg ;; esac done这里有些细节值得注意。SVG 是我首选的输出格式因为它是文本可以继续纳入后续处理也能保证放大不失真。PNG 只是给不需要编辑的外部协作方用的分发物不进入正式文档流。如果团队文档平台不直接支持 SVG再经由一个脚本统一转 PNG 输出。4.2 图也要过 lint语法、命名、引用三重校验写代码有 ESLint画图同样应该有 lint。我封装了一个.diagramlint脚本在提交前和 CI 中分别执行检查三类问题。语法校验是最基础的工具渲染失败直接报错。命名规范校验是查文件名是否符合业务域-描述.扩展名格式。引用校验则查图里的节点引用和common/下的主题文件是否存在、版本是否匹配。这块用 Python 写了一个不到两百行的检查器逻辑并不复杂关键在于把规则固化到流程里而不是靠人肉检查。import pathlib, re, sys errors [] for path in pathlib.Path(.).rglob(*.d2): if not re.match(r^[a-z]-[a-z0-9-]\.d2$, path.name): errors.append(f文件名不规范: {path}) content path.read_text(encodingutf-8) # 检查是否引用了不存在的局部变量 for line in content.splitlines(): if line.startswith(vars:) or line.startswith( ): continue if { in line and {not_defined} in line: errors.append(f未定义的变量引用: {path}:{line}) if errors: print(\n.join(errors)) sys.exit(1)这段代码只是个骨架实际规则会更复杂一些比如检查节点 ID 是否重复、连线两端是否真实存在于图中、非 ASCII 字符是否出现在 ID 位置。但从这个例子你可以看到lint 的本质不是技术问题而是把团队约定翻译成机器可执行的规则。4.3 Link 校验图与代码的对应关系检查lint 做完还没完我额外加了一道图-码一致性检查。这听起来有点玄但做法很朴素从代码仓库里提取服务名列表再和架构图渲染出的节点列表做比对。图中出现了代码里不存在的服务或者代码里已经删掉的服务还在图中赖着不走全部报警。这道检查把图表维护从自觉行为变成了强制行为。系统下线一个服务CI 里立刻标红架构图还引用了这个服务请同步更新。虽然是简单粗暴的字符串比对但效果出奇地好团队再也没有出现过图上有八个服务、代码里只有六个的离谱局面。数据来源可以灵活配置比如基于 Kubernetes Deployment 列表、基于注册中心的服务列表、或者基于代码仓库目录名。我用的是代码仓库目录名因为部署环境未必对开发环境开放但代码目录一定在手里。4.4 产物发布图档站点化的尝试构建产物不能只躺在 CI 的 artifacts 里。我将渲染好的 SVG 打包用 GitHub Pages 自动发布成一个静态图档站。按目录结构镜像展示每个图表页附带源文件链接、最近修改时间、和对应的代码 commit 号。这个站点同时作为团队内部的架构信息中心拿来做新人培训材料也很好用。这个做法的成本很低就是标准的 GitHub Actions 构建发布流程但收益远超预期。图表从夹在文档里的附件变成了一个可以浏览、检索、追溯的独立信息源。我在站内加了站内搜索支持按服务名、按业务域检索图表效率远超在网盘里翻文件夹。5. 实操中的几个深坑与绕行方案方案听着顺手落地过程其实踩了不少坑。挑几个有代表性的写出来给后面想复刻这套做法的朋友提前打预防针。5.1 中文字体与字符集引发的渲染事故第一次把 Mermaid 图部署到 CI构建直接失败错误信息指向文件编码。查了半天发现是 Windows 上保存的源文件是 GBK 编码而 CI 环境默认 UTF-8中文字符全变成乱码渲染直接崩。这个问题看起来低级实际很有普遍性团队协作中只要有一个同事用 Windows 老编辑器就会踩到。解决方案是在仓库根目录强制放置.editorconfig同时加一个 pre-commit 编码检查脚本非 UTF-8 文件直接拦截。具体到脚本层面核心就是检测非法字节序列# 检测非 UTF-8 编码文件 find . -name *.mmd -o -name *.d2 -o -name *.puml | while read -r f; do if ! iconv -f UTF-8 -t UTF-8 $f -o /dev/null 21; then echo 非 UTF-8 编码: $f exit 1 fi done还有一个隐藏更深的坑是字体。CI 环境的 Linux 容器里如果没有安装中文字体SVG 渲染出来全是豆腐块。别笑这个问题真实发生过而且极其隐蔽——本地 Mac 看起来一切正常CI 产物全部乱码。解法是在构建镜像里预装 Noto Sans CJK 字体包一劳永逸。5.2 节点一多布局就失控布局引擎的取舍与场景拆分Mermaid 自动布局在节点少于十五个时表现良好一旦节点数量多了连线交叉和节点重叠立刻变严重。我试过最大的一张架构图塞了四十多个节点渲染出来全挤在一起几乎不可读。这里有两种绕行方案我建议组合使用。方案一是换布局引擎。D2 可以切换dagre、elk、tala等不同引擎有些复杂图在 dagre 下一团乱麻换 elk 后立刻清爽很多。方案二更根本回到设计层解决——拆图。一张图超过十五个节点就必须拆成总览图模块详图两级结构。总览图只画模块间关系每个模块对应一张独立详图。这样单图复杂度可控布局不会失控阅读体验也更好。后来我把节点数超过 15 自动告警写进 lint 规则里凡超过的直接提醒作者拆图。这个数字不是拍脑袋定的是根据渲染效果和经验统计出来的阈值。5.3 团队习惯的对抗从画图者到建模者最后一个坑不是技术问题是人的问题。这套方案推行的前两周阻力非常大有同事明确表示用代码画图太慢还有人坚持说我画布拖一下比写代码快多了。我没有强行压制这种情绪而是做了一件事把高频图做成了模板化片段画图变成填空。比如流程图模板已经写好了开始节点、判定菱形、结束节点使用者只需要填步骤描述和连线条件。这样一来用代码画图的效率优势立刻体现出来——只要会填内容不用关心布局和样式渲染自动完成。更重要的是要把产出物可视化地展示出来。我特意在每周技术例会上把代码图渲染的 SVG 和旧版画布图并列展示视觉对比一目了然大家从代码慢的看法转向文档真好看的认同只用了不到一个月。其实一旦跨过这个习惯门槛团队就再也回不去了因为版本管理和自动维护的红利是实实在在的。6. 体系落地后的连锁反应图表治理向组织级演进当 diagram-design 跑通之后我意识到它的价值不止于替换工具而是从根上改变了团队处理信息的方式。6.1 架构评审语境从看PPT变为读代码过去架构评审大家围在一起看 PPT 上的架构图图代表的是设计意图和真实系统始终隔着一层。现在评审标题直接用渲染好的 SVG 图图的每一个节点都能回溯到代码仓库中的具体目录。评审不再需要问这里是这么实现的吗而是直接讨论这里为什么这么设计。这个变化非常微妙但意义重大。图的权威性大幅提升因为它不再是自由创作的示意图而是从代码结构映射出来的投影。任何人拿到一张架构图都可以顺着渲染链路一路查到对应的源码目录这本身就是强大的可信度背书。6.2 图表负债意识把过期的图当技术债务受代码技术债启发我提出了图表债这个概念。仓库里有一张三个月没更新的架构图就是一笔债因为它会误导新同学让他们按照错误的图理解系统。我建议团队把图表过期纳入技术债跟踪体系每次迭代排期时同步评估图是否需要更新。这个意识一旦建立文档新鲜度就成了一个被显式管理的事项而不是靠某个人记性好。现在就算某个域暂时没有代码改动我们也会定期巡检架构图与实际系统的差距把过期图视同 bug 处理。6.3 跨团队复用一套图语言覆盖多种角色很有意思的是这套体系不止研发团队在用。后来测试同学开始基于时序图补用例场景运维同学基于部署架构图做故障预案演练产品同学甚至开始用业务流程图来校对需求逻辑。因为图源文件是统一的结构化文本所有人都可以参与阅读和编辑图就从一个研发内部工具变成了跨角色协作的中枢语言。比如我随手画的一张用户登录时序图测试同学拿它来对照测试用例覆盖情况产品同学拿它和需求文档里的交互流程核对运维同学拿它分析链路里可能的单点故障。一张图同时服务了三个角色这是画布时代完全做不到的整合效应。