我做架构设计这些年,最怕的不是写代码,而是画图。不是那种随手画给同事看的意思,是正经交付给团队、写进设计文档、贴在 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。
| 维度 | Mermaid | PlantUML | D2 |
|---|---|---|---|
| 上手成本 | 很低,类 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(encoding="utf-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 2>&1; 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 跨团队复用:一套图语言覆盖多种角色
很有意思的是,这套体系不止研发团队在用。后来测试同学开始基于时序图补用例场景,运维同学基于部署架构图做故障预案演练,产品同学甚至开始用业务流程图来校对需求逻辑。因为图源文件是统一的结构化文本,所有人都可以参与阅读和编辑,图就从一个"研发内部工具"变成了"跨角色协作的中枢语言"。
比如我随手画的一张用户登录时序图,测试同学拿它来对照测试用例覆盖情况,产品同学拿它和需求文档里的交互流程核对,运维同学拿它分析链路里可能的单点故障。一张图同时服务了三个角色,这是画布时代完全做不到的整合效应。