ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

diagram-design:把画图变成写代码,让架构图成为工程资产

diagram-design:把画图变成写代码,让架构图成为工程资产

diagram-design 这个项目代号,听起来像是一个画图工具的玩具项目,但实际上它解决的是我在团队里一直头疼的问题——架构图、流程图、部署图永远躺在某个人的电脑里,要么是没更新的截图,要么是改不动的大文件。我把图表设计从“随手拖几个框”变成了一套可评审、可版本控制、可自动生成的工程流程,这也正是 diagram-design 的核心定位:把画图变成写代码,让图表资产真正参与协作与迭代。这篇文章会讲清楚为什么值得这么干、工具怎么选、一张系统架构图如何从零落地,以及我踩过的那些布局和渲染的坑。

1. 为什么要把“画图”当成“设计”来做

1.1 拖拽画图的隐性成本,比你想象的高

很多团队对画图的认知还停留在“能看就行”。新同学入职想看系统架构,老员工翻出半年前的版本截图,上面少了两三个新服务;产品要流程图,你临时开个画板,拖来拖去忙了半小时,结果导出个高清图片发过去,对方改了一个判断条件,你又要重新拖一遍。这些场景我全经历过,一开始觉得是沟通习惯问题,后来发现症结在工具和流程上。

传统的拖拽画图工具有一个共同问题:图和图的载体是分离的。图本身是二进制文件或私有格式,没法像代码一样做 diff,没法做行级评审,也没法在 CI 里自动校验。一个箭头改错了,你只能靠肉眼对比前后两张截图。更麻烦的是,一旦架构调整,图的更新往往靠“人肉记忆”,没人记得这张图藏在哪个网盘路径下。久而久之,架构图就成了一次性的“历史照片”。

这种成本通常不会被纳入项目排期,但它确确实实每天都在消耗团队信任。新同学照着过期的图去理解系统,踩坑之后反过来怀疑文档;专家评审时发现某个依赖画错了,整个讨论方向都会跑偏。所以我把 diagram-design 这个项目定为“工程化”而不是“画图”,因为图表最大的价值不是展示,而是准确传达一组关系,这张图必须能跟代码库同步演进。

1.2 图表即代码,是解决“图腐化”的关键思路

图形即代码(diagrams as code)并不是新概念,核心就一句话:用声明式文本来描述图中的节点、连线和关系,再由工具渲染成 PNG、SVG 或 PDF。这样做最直接的收益是版本管理。文字文件天然支持 Git diff,每次改动都能看到是哪条线变了、哪个节点加了,评审的时候像 review 代码一样 review 图,这个体验是拖拽画板给不了的。

代码化还有一个容易被忽视的好处:可复制与可复用。有了文本源文件,你换新机器、换团队成员、换文档平台,图都能原样生成,不会因为没装某个软件就打不开。样式可以抽成变量,公共节点可以抽成模块,多张图共享同一套视觉规范,维护成本直线下降。

我自己体会最深的是“自动布局”带来的效率提升。手动画图时,加一个节点要把整张图擦掉重排;代码化后,你只需要在文本里加一行,布局引擎会自动把画布重排,省掉大量手工对齐时间。当然,自动布局也有它的脾气,具体怎么调后面会专门说。

1.3 工具选型:D2、PlantUML、Graphviz 怎么选

既然是“图表即代码”,那你选的就是一门描述语言外加一个渲染引擎。市面上常见的方案各有侧重,我结合团队实际情况做了对比,最终选了 D2 作为主力。

工具语法特点适合场景选型提示
D2声明式、极简,节点关系用箭头表达系统架构图、云架构图、网络拓扑上手最快,布局引擎自动能力强,默认输出很现代
PlantUML内置多种 UML 类型时序图、用例图、类图、活动图Java 体系项目里很常见,UML 标准支持最全
GraphvizDot 语言,属性控制粒度细复杂依赖图、大规模集群关系灵活度最高,但需要花时间调布局属性

选择逻辑上,如果你团队大多数是业务研发,画图频率不高,哪怕半年画一次,也应该优先选语法简单的;如果你们特别强调 UML 标准,比如要做严格的活动图交给 CMMI 评审,那 PlantUML 是稳妥的选择;如果图规模大、且你要精细控制连线走向,Graphviz 更合适。

我这里以 D2 为主展开,因为它对新人足够友好。哪怕你没写过一行绘图代码,跟着下文的示例走一遍,也能在半小时内做出第一张像样的架构图,这个学习曲线是传统画板完全不具备的。

提示:别被“选择困难”拖住,工具只是手段。真正的重点是先把图的关系整理清楚,工具反而是最不重要的环节。

2. 核心细节解析:语法与布局设计

2.1 从节点关系到画布:D2 的语法有多直白

D2 的语法我一开始都怀疑自己看错了,因为它简单到“不像一个绘图工具”。你只需要写名词当节点,用箭头当关系,工具就能画出一张图。拿最经典的场景举例:

direction: right cloud_assets: 静态资源 app_server: 应用服务器 api_gateway: API 网关 database: 核心数据库 cloud_assets -> app_server: 加载资源 app_server -> api_gateway: 业务请求 api_gateway -> database: 读写数据

这段文本生成图的流程很简单:节点A -> 节点B表示一条连线,冒号后面是线条上的注释。没有坐标,没有拖拽,所有内容都是文本。这种语法就像写提纲一样,脑子里先列几个关键名词,再用箭头把关系串起来,剩下的布局交给引擎处理。

D2 里比较重要的概念是一个叫“容器”的东西。你可以把一组节点包在一个大块里,表示它们属于同一个子系统或服务组:

direction: right user_end: 用户端 { mobile: 移动端 desktop: 桌面端 } biz_core: 业务核心 { order: 订单服务 user: 用户服务 }

里层节点用父容器.子节点的方式引用,比如user_end.mobile -> biz_core.order。这个能力非常实用,因为真实系统的架构永远是分层的,没有容器的图最后会变成一堆平铺的节点,谁跟谁也分不清。容器在视觉上是带背景的大矩形,天然承担“分组”和“边界”两个信息,一张图里如果出现了三四个容器,整体阅读节奏会清晰很多。

另外,D2 还支持定义变量和引用外部模块。变量用vars声明,模块用@import引入,适合把公共的存储节点、中间件定义抽出来,让每张图保持一致。这也是代码化比画板走得远的地方。

2.2 布局引擎:自动布局是省心还是添乱

自动布局听起来很省心,但用过的人都知道,引擎给的布局不总是你脑子里的那张图。D2 内置了多个布局引擎,不同引擎背后的算法取向不同,应用场景也不一样。

  • DAGRE(默认):适合有向无环、层级清晰的依赖图,比如“网关 → 服务 → 数据库”这种自上而下的调用链。
  • ELK:适合图规模较大、连接关系较多的情况,稳定性和交叉线的控制更好。
  • TALA:适合强调分层关系的图,例如不同层次的系统自上而下的排列。

我最初画一张跨系统调用关系图,节点 20 来个,连接 30 多条,默认布局出来线交叉得厉害,怎么读都别扭。后来发现问题在于这张图里有环——服务 A 回调服务 B,B 又依赖 A,这种互相拉扯的关系让 DAGRE 引擎只能强行排布,效果自然不会好。

解决这类问题,我建议记住两个原则。第一,图里尽量避免环,实在有环就拆出来单独画时序图;第二,善用方向和分组,比如从direction: right改成direction: left或down,很多时候光改方向就能让布局清爽一半。方向不对的图,引擎再努力也别扭。如果你追求的是系统架构图,我强烈建议先用down自上而下,浏览器和服务器从上到下排列,符合绝大多数人的阅读习惯。

还有一个经验是节点数量要控制在合理范围。30 个节点以内,默认布局引擎基本靠谱;超过 50 个,自动布局很难不变成一团乱线,这时候应该考虑拆图,而不是继续往上堆。

2.3 样式与主题:从“有线框图”到“有设计感”

代码化绘图有个先天优势:样式完全可控,而且可以通过变量统一管理。但很多团队生成的图一眼看上去还是丑,原因通常是颜色没有语义,线条没有规范。

我接手 diagram-design 之后给自己定了一套规则:容器背景色按层次区分,例如接入层用冷色系,业务层用暖色系,数据层用中性色系;普通节点用白底,核心节点用深色底加白色文字;实线表示强依赖的同步调用,虚线表示异步消息或弱依赖。这样一套规范沉淀下来,图的辨识度立刻提升,不需要靠图例解释就能看懂大半。

D2 里设置颜色非常简单,节点名后面直接给属性即可:

gateway: API 网关 { style: { fill: "#D7E6FF" stroke: "#2E5FA3" font-color: "#153A5B" border-radius: 8 } }

但如果每一条样式都写这么长,图一多就累赘了。更好的做法是用vars定义全局颜色变量,后续只改一处,全图生效:

vars: { color_access: "#D7E6FF" color_core: "#FFF0D4" color_data: "#E8E4F1" }

我实际用下来极爽的场景是切主题:D2 内置好几个主题,团队审美变了或者要给客户出稿,一行命令切换风格,完全不需要重新画。图表具备“可换肤”能力,这在传统画板里几乎不敢想象。

3. 实操过程:从零构建一张系统架构图

3.1 先画拓扑还是先列依赖?顺序搞反必返工

很多人拿到需求的第一反应是打开编辑器开始画框,我强烈不建议这么干。图形只是表达形式,真正要梳理的是实体和关系。就像一个作家不会上来就写小说大纲,先得想清楚人物关系和历史背景。

我在 diagram-design 项目里画的第一张图,是一个模拟跨平台系统的架构图。当时整理了三个维度:

  • 边界:系统对外提供什么能力,依赖哪些第三方或公共组件。
  • 组件清单:有哪些终端、网关、服务、存储、中间件,先全部列出来。
  • 关系图:每对组件之间是同步调用、异步消息,还是只读依赖,标清楚。

这些清单可以先写在文本草稿里,甚至写在便利贴上。等清单确认无误,再写 D2 代码,基本一次成型。反过来,如果你先着急画图,后面发现漏了一个关键依赖,你会发现整个布局跟着崩掉,返工成本成倍增加。

3.2 从零到一:完整代码与迭代过程

还是拿这个模拟系统举例。第一版我写了最简单的 10 个节点,没用容器,也没分组,代码长这样:

direction: right mobile: 移动端 desktop: 桌面端 gateway: 接入网关 biz: 业务中台 user: 用户中心 order: 订单中心 db: 数据库 cache: 缓存 mq: 消息队列 mobile -> gateway desktop -> gateway gateway -> biz biz -> user biz -> order user -> db user -> cache order -> db order -> cache order -> mq

用默认参数d2 input.d2 -o output.svg生成之后,图能看懂,但问题是所有节点平铺,层次感不够,线也比较绕。接着我引入容器,给接入层、业务层、数据层分别划了组:

direction: down mgmt: 接入层 { mobile: 移动端 desktop: 桌面端 gateway: 接入网关 } core: 业务层 { biz: 业务中台 user: 用户中心 order: 订单中心 } storage: 数据层 { db: 核心数据库 cache: 缓存集群 mq: 消息队列 } mgmt.mobile -> mgmt.gateway mgmt.desktop -> mgmt.gateway mgmt.gateway -> core.biz core.biz -> core.user core.biz -> core.order core.user -> storage.db core.user -> storage.cache core.order -> storage.db core.order -> storage.cache core.order -> storage.mq

这一版的阅读体验明显改善,但因为容器、层次多了,我加上了颜色变量来区分层级。于是定义:

vars: { c_border: "#2E5FA3" c_bg: "#F2F7FF" }

核心节点用更深一档的颜色,并把关键链路说明写进连线注释里:

mgmt.gateway -> core.biz: 鉴权 + 路由 core.order -> storage.db: 读写订单主数据 core.order -> storage.mq: 发送订单事件

这一版提交到 Git 后,评审同事对着一张图能直接提出“订单数据落库之后,缓存为什么不先失效?”这种对业务正确性的质疑。这就是代码化绘图真正的价值:评审者看到的不再是一张静态图片,而是一组可讨论的关系,异议自然会冒出来。

3.3 把图嵌入文档、CI 与团队规范

图能本地生成只是第一步,我更看重的是把图表接入协作流程。文档平台如果支持 SVG 上传,那就直接传生成的 SVG,细节清楚且不糊图。如果需要内嵌到静态站点,用命令把 D2 转成 SVG 或 PNG 即可,配合脚本还能批量导出整个 docs 目录下的图纸。

我在团队里还跑了一个简单脚本,每次有图表文件变更,自动重新生成图片并提交到文档目录。加了个节点、改了一条线,本地执行一条命令之后,文档里的旧图会自动被替换,不再需要手工导出上传。这一步能把图的维护成本压缩到接近零,团队成员才愿意持续维护图,而不是画完就扔。

另外,我强烈建议给团队的图表设计写一份“一会儿需求”式的规范。不用很长,核心就几条:架构图用什么颜色体系,连线虚线实线分别代表什么,节点命名用不用中英文,标题层级怎么写。规范不需要一步到位,可以从遇到一次沟通歧义就补一条开始,慢慢沉淀。我第一次给团队定规范时只有四行字,这个行为本身就是一种“把图当资产看待”的信号。

提示:图虽然是一门“语言”,但它也需要可读性。每张图开头加一行图例说明,是成本极低、收益极高的动作。

4. 常见问题与排查技巧实录

4.1 布局乱飞、节点重叠,怎么把它捋顺

我收到最多的反馈是“自动布局位置很奇怪”,比如线交叉成一团、节点堆积、方向不符合预期。这类问题多数不是工具出错了,而是图和引擎之间的“期望错位”。

先检查有没有环。单向调用链自动布局一般没问题,一旦出现双向依赖,引擎就会把连线绕来绕去。这时优先重构关系,把环打破;如果实在绕不开,考虑换布局引擎,用命令行参数指定-l,不同的引擎对环的容忍程度差异很大。

再看方向。系统架构图我习惯用direction: down,让网关在最上、数据在最下,像瀑布一样自上而下流动。模块依赖图我偏向direction: right,从左侧入口一直延伸到右侧出口。方向不匹配,图再美也难读。

最后看分组。节点一多就尽量放进容器里,让相关实体在视觉上互相靠近。容器相当于给了自动布局一个“先画组内、再排组间”的提醒,能显著减少跨区域的长连线。如果已经是容器嵌套还乱,可能是容器层级太深,超过两层建议拆图。

4.2 中文字体与渲染问题,小坑一个接一个

中文是代码化绘图绕不开的坎。默认渲染环境下,字体缺失会导致中文变成方框,这在国内团队几乎都会遇到。解决办法是给 CLI 指定中文字体路径,或者提前在目标机器上安装好字体。字体问题的另外一个隐蔽点是不管用 PDF 还是 SVG,字体没有嵌入时,发给别人打开就是错位,建议导出 PDF 时务必检查字体是否嵌入。

还有一个细节是输出格式的选择。SVG 适合网页和文档,放大不糊,但某些文档系统会默认把 SVG 当图片处理,内部字体出问题最好尽早测试。PNG 适合给外部客户预览,但要指定足够的分辨率,避免缩放后边缘发虚。PDF 适合评审汇报,整体排版稳定。我在交付客户方案时一般导 PDF,内部文档用 SVG,展示用 PNG,按需切换比一把梭到底靠谱得多。

4.3 图太大、节点太多,拆图有讲究

节点超过 50 个,不管什么布局引擎都很难维持优雅,这时候要果断拆图。拆图不是简单按数量切两半,而是按视角拆。比如一张“系统全景图”只需要画边界和关键子系统,每个子系统内部的详细依赖,另外画一张“模块依赖图”;一次请求的完整链路,又单独画一张“时序图”。三张图各司其职,比一张塞满所有细节的大图有效得多。

我还有一个搭配做法是给全景图里的子系统加上编号,对应到子图文件名,并在文档里把这几张图串起来。读者先看全景图建立认知,再按编号跳到对应细节图,阅读路径清晰,且每张图都保持在可维护的规模内。

4.4 日常问题速查表,直接对着抄作业

症状可能原因处理方案
中文变方框目标机器缺字体安装中文字体,或在 CLI 中指定字体文件
连线严重交叉图里有循环依赖尽力破环,或换布局引擎,或调方向
节点顺序不对容器分组没做,层级缺失用容器划分层次,再调方向
导出图片边缘发虚PNG 分辨率不够提高分辨率,或改用 SVG 输出
文档里的图更新不及时手工导出太麻烦写脚本自动重新生成,并接入提交钩子
多张图风格不统一每个文件硬编码颜色建立公共变量文件,统一引用

这张表不针对特定一两个问题,而是把最奖励验证过的经验放进来。遇到布局问题先别急着画新图,对照着看一眼,哪条对应就调哪条,往往一次就解决。

4.5 从“会画图”到“设计图”:规范是护城河

工具用熟了很容易产生“画得真快”的错觉,但不立规则,团队协作还是会乱。真正让 diagram-design 这个项目站稳脚跟的,是把它沉淀成一套团队内部规范。

规范可以很小,但一定要约定清楚:线型语义(实线等于同步强依赖、虚线等于异跖弱依赖)、颜色语义(不同层级不同色系)、容器使用原则(超过 10 个节点必须分组)、命名风格(中英文统一,避免大小写混用)。有了这些约定,一张图本身就是一份文档,不需要额外的长篇解释。

遇到争议时,我习惯快速画两版比较。一版“按规范来”,一版“按直觉来”,拿去给同事投票,结果往往一面倒支持规范版。一旦经历过这种对比,团队对规范的认同度就会迅速提升。设计图不单要“画出来”,还要“定下来”,否则再漂亮的图也只是个人一时灵感,无法沉淀成团队资产。

注意:规范不是拿来炫技的,是拿来兜底的。图最重要的读者是新同学和未来的你,请用最简单的规则让图在半年后依然读得懂。

我个人在实际操作中的体会是,diagram-design 这个项目最难的点不是语法,也不是布局,而是让人把图当成会演进的资产来对待。过去大家习惯画完即丢弃,现在每张图都有源文件、有变更记录、有评审评论,它跟代码一样在呼吸。慢慢地,你会发现团队讨论问题时开始主动说“去看那张图”,而不是“有没有截图发我一份”。这大概就是图表从“一次性工具”走向“工程资产”最朴素也最真实的变化。如果你也在为团队里那些散落各处的架构图头疼,不妨从一张最小可用的代码化图开始,先让流程转起来,后面的事都会顺理成章。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进