ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用代码画图:Diagram-as-Code 从选型到落地工作流的完整指南

用代码画图:Diagram-as-Code 从选型到落地工作流的完整指南 画图这件事看起来简单做起来全是坑。尤其是做系统架构图、业务流程图、拓扑图这类偏「技术图纸」的内容大多数人的状态是打开一个画图软件手动拖拽方框连线改一个字段名就要拆半边图改完布局全部乱掉最后不得不推倒重来。我前前后后因为这种返工浪费了大量时间之后彻底转向了diagram-design这套思路——把图当成代码来管理用结构化的文本描述关系让布局算法去处理坐标再配合画布型工具做精修和演示。这篇文章我把自己从工具选型、布局原理到落地工作流的完整实践记录写出来给正在被画图折磨的人一个可以直接抄作业的参考。1. 先讲透一件事图表设计的本质是信息排序1.1 为什么你的图总是没人看得懂先说一个扎心的结论大多数人画的架构图、流程图问题根本不在于「不会用工具」而在于「没想清楚要表达什么」。你打开 Visio 或者 Excalidraw 的那一刻脑子里其实只有一个模糊的轮廓然后就凭感觉把能想到的模块一个个摆上去。等摆到十几个节点线一多图就开始失控。我自己复盘过很多次画图翻车的场景发现看不懂的图几乎都有这几个共同点边线交叉严重整个图看起来像一张蜘蛛网读者视线根本不知道从哪里开始。同类的节点散落各处。比如一个系统有四个微服务你随手摆在了画布左上、右下、中间各一个读者很难意识到它们是同一层的东西。方向混乱。箭头一会儿从左往右一会儿从下往上一会儿又从右往左。人眼阅读是有习惯顺序的方向不一致等于给读者制造阅读障碍。层级信息缺失。哪些是上层应用、哪些是基础组件、哪些是外部系统图上完全没有体现。你以为你在传递信息实际上你只是把自己脑子里的一团乱麻原样转移到了画布上。1.2 一张好图的隐含标准路径最短、分组清晰、方向一致我把图理解为「信息排序」之后画图的逻辑就变了。一张合格的图表本质上要满足三个隐含标准第一路径最短。任意两个有关联的节点它们之间的连线路径应该尽可能短而且尽可能不穿越其他节点的「领地」。这个标准在交通线路图、电路图、拓扑图中是工程师的硬性要求因为路径越长读者跟踪这条线索消耗的注意力就越多。一张 50 节点的架构图如果每一条线都绕来绕去读者的耐心会迅速耗尽。第二分组清晰。同一层级的模块要聚在一起同一领域的组件要能被一眼识别出来属于同一个集合。这就是为什么几乎所有专业的图表工具都有「子图」「分组」「容器」这类概念——比如 Graphviz 里的 cluster、D2 里的 container、Mermaid 里的 subgraph。分组的本质是把图上的一维线性关系升级为二维空间关系这样才能承载更多信息。第三方向一致。要么整体自上而下表示调用层级要么整体从左到右表示业务流程不要混用。方向一致不仅是一种审美更是一种阅读约定。读者不用思考「这条线为什么往上走」可以把全部注意力放在内容本身。想清楚这三点之后再回头看工具选型就简单多了——凡是能辅助你满足这三个标准的工具都值得用凡是需要你手动去调整节点位置才能勉强满足这三个标准的工具都只适合做小图精修不适合做大图治理。2. 工具选型地图代码生成型与手工画布型各有各的适用边界做 diagram-design 绕不开工具选型。市面上的图表工具看着五花八门但本质上只分两大类代码生成型Graphviz、Mermaid、D2、PlantUML和手工画布型Excalidraw、tldraw、Figma、Visio。这两种类型的设计哲学完全不同适用场景也完全不同。很多人纠结选型其实是在用错误的维度比较这两类工具。2.1 两大类工具的核心差异代码生成型工具的核心思路是你用文本描述节点和连线关系然后由布局引擎自动计算节点的坐标位置。你写的不是「图」本身而是图的「抽象描述」。手工画布型工具的核心思路是你直接操作画布上的图形通过拖动、对齐、吸附来精确控制每个元素的位置。你画的才是图本身。这个差异带来一个非常关键的推论代码生成型工具维护成本极低但精确控制能力弱手工画布型工具精确控制能力强但维护成本极高。你想改一个节点名字代码型工具改一行文本重新渲染就完事画布型工具可能要手动调整与之相连的所有线的位置甚至重排版式。我把两类工具的核心特性整理成了一张对比表供你直接参考维度代码生成型Graphviz/Mermaid/D2手工画布型Excalidraw/tldraw/Figma布局控制交给布局算法只能通过参数干预完全手动像素级控制版本管理图源文件是纯文本可进 Gitdiff 清晰二进制或私有格式难以 diff修改成本改一行文本即可重新生成需要手动调整受影响的所有元素上手门槛需要学一点 DSL 语法基本零门槛拖拽即会适合场景架构图、拓扑图、文档内嵌图、自动化生成图探索期草图、白板讨论、高保真演示图颜值上限取决于主题和自定义能力上限很高但全看个人排版功底大图承载力强几百节点也能勉强渲染弱超过几十节点基本没法维护2.2 代码生成型工具各自的长短板如果你决定走代码生成这条路线下面这几款工具是我实际用过的可以直接说结论。GraphvizDOT 语法是这一行的老牌工具它的dot布局引擎至今仍是很多图布局算法的基准。DOT 的语法非常底层详细程度高但啰嗦样式偏老气默认输出差不多是上世纪九十年代的科技论文风格。但它的布局能力是真的强尤其是有向无环图DAGdot引擎排出来的层级结构非常规整。如果你要画的是依赖关系图、调用链图Graphviz 依然是首选。Mermaid是文档界的事实标准因为 GitHub、Notion、很多 Markdown 工具都原生支持它。它的语法最简洁写起来几乎零学习成本graph TD这种声明式写法非常符合直觉。但 Mermaid 的布局引擎能力有限节点一多、边一多布局就会开始摆烂。而且 Mermaid 对复杂自定义样式支持一般想精细控制节点颜色、边框、容器样式要写大量配置性价比不高。所以我的判断是Mermaid 适合在文档、评审 PPT 里快速画一张轻量级图不适合作为正式图表资产的管理工具。D2是这堆工具里的新玩家也是我目前最偏好的一个。它吸收了 DOT 的布局能力语法设计比 DOT 友好得多而且样式系统现代化。D2 支持变量vars、自动布局、容器嵌套还能直接导出 SVG/PNG。更关键的是D2 对「大图」场景做了很多优化比如steps分步展示、scenario场景切换这对做架构演进图特别有用。如果你愿意折腾D2 是目前代码生成型工具里综合体验最好的选择。PlantUML的强项是 UML尤其是时序图、类图、活动图。如果你画的是专业软件建模图PlantUML 依然是绕不开的选择。但如果只是画普通的架构图、流程图PlantUML 的语法负担和布局观感都不如 D2 灵活。2.3 我的选型逻辑按使用场景分流不搞「一个工具走天下」我现在的方案是三工具配合正式的架构图、拓扑图、依赖图用D2源文件进 Git 仓库。文档里需要快速内嵌一张轻量图用Mermaid方便同事直接阅读。方案探讨、协作白板、演示用的高保真图用Excalidraw或tldraw。这个组合里没有 Graphviz 和 PlantUML不是它们不好而是它们在我当前的场景里没有不可替代性。但如果你做的是依赖分析或者 UML 建模这两个工具还是要优先考虑。选型的核心逻辑就一句话图的生命周期决定了你的工具选择。画一次就完事、后面不会再改的图用什么工具都行挑顺手的。会被频繁修改、持续迭代、多人维护的图必须选择「描述即代码」的方案否则改到第三版你就会明白什么叫「画图半小时改图两小时」。3. 布局引擎到底在干什么——把「图为什么总是乱」彻底讲明白3.1 dot 布局有向图的层级拓扑与同层对齐很多人对代码生成型工具最大的误解是以为布局引擎会「智能」地把图画好看。实际上布局引擎做的事情非常机械——它只是在努力解决一个数学问题给定一组节点和一组边如何在这些节点不重叠的前提下让边交叉尽量少、边长尽量短、布局尽量紧凑。以 Graphviz 的dot引擎为例它专门针对有向图设计。它的工作过程大致是先把所有节点按照边的方向进行分层。比如 A 指向 BB 指向 C那 A 在第一层B 在第二层C 在第三层。然后调整同一层内的节点顺序目标是减少连线交叉。最后计算所有节点的具体坐标让整体布局平衡。听着挺合理对吧但它的前提条件是你的图必须是一个有向无环图DAG。如果你的节点之间存在循环依赖比如 A 指向 B、B 指向 A那 dot 的分层逻辑就会陷入混乱它需要通过断边或者反向处理来强行分层出来的布局经常非常奇怪。理解了这一点你就能明白为什么有时候 dot 布局出来的图丑得离谱——不是引擎不行是你的图结构本身就不符合它擅长的类型。如果你的图是无向图或者存在大量环应该试试neato或fdp这类力导向布局引擎。3.2 力导向布局弹簧模型与它的适用范围力导向布局Force-directed layout是另一个重要的布局思路。它把每个节点想象成一个带电粒子节点之间存在引力由关联关系产生和斥力防止节点堆叠。算法通过迭代计算让整个系统的能量趋于最小最终达到一种平衡状态。Graphviz 里的neato和fdp就是典型的力导向布局引擎。D2 的默认布局器dagre虽然主要处理层级布局但也借鉴了很多图布局的优化思路。力导向布局的好处是它对图的结构没有太强的预设环形结构、星型结构、网状结构都能处理得比较自然。但它有一个致命问题布局结果不稳定。你只是加了一个节点重新跑一遍布局之后其他所有节点都可能挪窝。这就意味着用力导向布局画出来的图每次渲染结果都可能不一样不适合做需要稳定版式的图表资产。所以我的经验是尽量用层级布局来表达有方向的依赖关系用画布型工具手动布置才是无向网络拓扑的正解。力导向布局可以作为探索一个陌生数据集的起点但别指望它给你一张能直接交付的图。3.3 边交叉与跨层边布局算法解决不了的得靠人布局引擎能做的优化说到底只是一堆启发式规则。当图规模增大到一定程度交叉边的数量无论如何优化都会变得难以接受。我遇到过最多的情况是一个 20 节点左右的架构图关系比较复杂任意两个业务模块之间都有交互。这种情况下无论你用dot还是dagre出来的布局都会是一团乱麻——因为这张图本身的信息负载就已经超出「一张图能清晰表达」的上限。这时候布局引擎帮不了你你得自己动手降低信息密度。常用的手段是「引入中间层」和「聚合抽象」。引入中间层的意思是不要画「蜘蛛网」而是给这 20 个节点之间再加一个「公共依赖」层比如统一走 API 网关节点之间不再直接连线而是各自连接到网关。这样边的数量直接从 O(n²) 降到 O(n)布局瞬间清爽。聚合抽象的意思是把本来就属于同一子系统的多个节点折叠成一个容器节点。读者先看大结构需要细节时再进入子图看局部。这正是我在后续章节要展开的大图治理思路。记住一句话图上每多一条多余线段读者理解成本就高一分。删边和加边同样重要。4. 用代码把图管起来一套可落地的 diagram-design 工作流4.1 目录结构与版本管理图也是代码资产选定了代码生成型工具之后接下来最重要的事情就是把图纳入正式的工程化管理流程。图源文件应该和代码放在同一个仓库里走统一的评审、合并流程。我当前的推荐目录结构是这样的diagrams/ ├── README.md # 图表索引写清楚每张图对应哪个文档/系统 ├── 01-architecture/ # 按主题分目录 │ ├── core-system.d2 # 源文件 │ ├── core-system.svg # 导出产物 │ └── core-system.png ├── 02-workflow/ │ ├── dev-ops-pipeline.d2 │ ├── dev-ops-pipeline.svg │ └── dev-ops-pipeline.png └── assets/ └── themes/ # 共享样式定义 └── brand.d2有几个实践细节值得强调源文件和导出产物分开管理。源文件负责演进导出产物负责展示。如果你用的是支持图源即图片的渲染服务导出产物甚至可以不进 Git但为了方便在 PR 里直接预览我还是会把 SVG 一并提交。每张图配 README 索引。不少项目过半年之后图库里堆了几十张命名含糊的final_v3根本没人知道哪张图是当前有效的。README 索引只需简单写清「图名、用途、对应模块、更新日期、责任人」就能避免这个混乱。严格执行代码评审。图也是代码资产也应该进 Code Review。评审图的时候重点看的是「关系描述是否正确」「是否有更好的分组」「是否有可删的边」而不是「颜色好不好看」。D2 这类文本语法天然适合 diff 审查这也是我强烈建议用文本型工具管理正式图表资产的根本原因。4.2 语法与命名规范怎么写才不会越写越乱文本型工具只是技术底座真正决定图的质量的是你写「图代码」时的规范程度。下面是我自己总结的一条硬性规则节点命名必须语义化永远不要用中文做节点 ID永远不要直接在标签里堆砌长文本描述。D2 的语法示例大概是这样的vars: { d2-config: { theme-id: 200 layout-engine: dagre } } api-gateway: API 网关 user-service: 用户服务 order-service: 订单服务 payment-service: 支付服务 database: 主数据库 { user-db: 用户库 order-db: 订单库 } api-gateway - user-service: HTTP api-gateway - order-service: HTTP api-gateway - payment-service: HTTP user-service - database.user-db: SQL order-service - database.order-db: SQL注意这里的几个要点节点 IDapi-gateway、user-service使用小写连字符命名它是图的「内部地址」不允许包含空格或中文。label双引号中的文本才是展示给读者看的内容。容器database内部的子节点通过database.user-db这种点语法引用层级关系一目了然。边的标签HTTP、SQL只写「动词或协议」不要写长句子。边标签的意义是解释两个节点之间的交互关系而不是描述整个业务的来龙去脉。我见过太多人写 Mermaid 或 D2 的时候把一整个需求描述写进节点 label最后图面上全是长段文字几乎没有留白。这里的原则是节点标签控制在 5 个汉字以内边标签控制在 3 个词以内。做不到这点说明你的图需要拆分而不是硬塞。4.3 构建与发布命令行导出与自动渲染代码生成型工具的一大好处就是可以无缝集成到自动化的构建流程里。以 D2 为例日常导出命令非常简单# 生成 SVG d2 --theme 200 architecture/core-system.d2 architecture/core-system.svg # 生成 PNG需要另外安装 vega-cli也可以用 SVG 转 PNG 工具 d2 --theme 200 --format png architecture/core-system.d2 architecture/core-system.png在 CI 里你可以给图库单独开一条流水线PR 合并到主干之后自动渲染所有.d2文件并刷新 README 索引。这样团队里的任何人都无需本地安装额外工具就能拿到最新的 SVG 图。用 Graphviz 的话核心命令是dot -Tsvg input.dot -o output.svg用 Mermaid 则一般通过mmdcmermaid-cli转换。所有这一系列命令都建议丢进 Makefile 或者npm scripts统一入口render: find diagrams -name *.d2 -exec d2 --theme 200 {} {}.svg \;我在实际落地过程中踩过一个大坑特别值得提醒中文字体渲染问题。在 CI 容器里跑渲染默认环境往往没有安装中文字体导出的 SVG 里所有中文都是豆腐块方框。解决方式是在 Dockerfile 里显式安装fonts-noto-cjknoto 中文字体并刷新字体缓存。这个问题在本地通常不会出现但在自动化环境里极其常见几乎每一个把图表纳入 CI 的团队都会遇到一次。另一个经验是SVG 是首选交付格式PNG 才是备选。SVG 是矢量格式放大不糊PNG 在非矢量场景比如嵌入网易等不支持 SVG 的编辑器里才需要。如果你用的是 Markdown 文档体系尽量嵌入 SVG如果你要贴到聊天软件里导出 PNG 再贴。5. 图表设计的实战细节那些看着小、但决定成败的点5.1 配色与语义一旦颜色承担含义就必须有图例我见过太多架构图作者用红色标出「重点模块」用蓝色标出「已废弃模块」用绿色标出「新建模块」然后整张图没有任何图例说明。读者看到这张图只会觉得你是在随便涂色。颜色在图里的角色有两种一种纯粹是装饰为了让图更好看另一种是语义承担了「传递状态/层级」的作用。只要你的颜色承担了语义就必须同时给出图例。在实际操作中我建议把颜色控制在 3 种以内并且每种颜色对应一种明确含义。比如主蓝色表示「核心业务模块」。灰色表示「基础设施/第三方依赖」。橙色表示「本次改动的模块」或「高风险模块」。这套配色还要在团队的多个图之间保持一致性所以建议把颜色定义抽成共享变量。在 D2 里可以这样做vars: { color-primary: #2D5AF0 color-neutral: #95A0B5 color-warning: #FF8C2A }如果你今天画的图用了三套颜色明天画的又换了一堆读者每次读图都要重新学习你的「颜色语言」代价极高。一致性的优先级永远高于单张图的美观。5.2 文本宽度与节点尺寸为什么你画的图总是被截断这是代码生成型工具里最容易让人抓狂的问题节点的显示宽度和文本长度不匹配。Graphviz 的节点尺寸默认不会根据文字长度自动扩展尤其是使用中文 label 之后五六个汉字就可能超出圆形节点或固定宽度节点的边界文字直接溢出或者被截断。如果你必须用 Graphviz 家族解决方式是给节点设置宽松的width和height属性把节点造型调整为矩形DOT 默认是椭圆对中文更不友好或者使用 HTML-like label 来做换行digraph G { node [shapebox, stylerounded, margin0.2,0.1] api网关 [width1.8, height0.6] }D2 在这方面做得比较好它默认会计算文本长度来动态调整容器尺寸所以中文截断问题较少。但如果你在一个容器里塞了很长的文字它也会把布局撑得很大到时候还是要回到「标签精简」这条老路上来。实际上节点尺寸问题的根源不是工具是我前面强调的文本长度。一个节点标签超过 10 个汉字说明你在拿图当表格用。图是给人看关系的不是给人读段落的地方长文本放到文档正文里。5.3 大图治理超过 100 个节点之后怎么办代码生成型工具虽然理论上能渲染大量节点但一旦节点数超过 100图的可读性就会断崖式下降。哪怕布局算法把它排得非常规整人类的阅读能力也处理不了 100 个节点的图——没人能在一张图里同时跟踪多条路径并保持清晰的脑内模型。大图治理只有两条路拆分和分层。拆分的意思是把一张 150 节点的图按时序或按子系统拆成 3-5 张 30 节点左右的子图每张子图只讲一个主题。然后在总览图里用容器表示这些子系统容器之间画交互线。读者先看总览建立全局认知再看子图了解细节。分层的意思是通过「聚合节点」把大图变成可下钻的多层级结构。D2 天然支持容器嵌套你可以在外层画一个user-service容器在容器内部再展开它的内部实现细节。这种「总览-下钻」模式是把大图变小的唯一正道。这里也分享一个真实的教训我曾经试图把公司的全链路系统包括前端、网关、十几个微服务、消息队列、多类存储、外部依赖画成一张大图花了一整天排好版结果评审会议上根本没人能读懂。后来我拆成「架构总览」「核心调用链」「数据存储」「外部依赖」四张图每张图不超过 40 个节点评审效率才真正提上来。一张图企图表达的东西越多它实际传达的信息就越少。6. 进阶从静态图到可交互的图解系统6.1 在网页里嵌入交互式图表代码生成型工具导出的 SVG 是静态的但你可以通过 SVG 的交互能力把它变成可交互的图表。D2 导出的 SVG 会给每个节点和边加上id属性这意味着你可以直接用 JavaScript 给这些节点绑定事件。最简单的做法是在网页里加载 SVG 文件后给所有node元素绑定点击事件点击某个服务节点时高亮所有与它直接相连的节点和边同时让无关节点降低透明度。这种「聚焦邻接」交互对排查问题、理解依赖关系极其有用比静态图的信息传达能力高一个量级。具体的做法取决于你用的渲染器。如果是 D2 的静态 SVG你可以在 SVG onload 事件里操作 DOM如果你需要完全动态的渲染可以考虑 D2 的 JS 运行时d2 官方提供 wasm 编译版本在浏览器里直接根据数据源渲染和更新图。6.2 数据驱动把实时状态映射到图节点上更进一步的做法是让图不再是一张固定不变的画而是一个随数据实时变化的状态面板。这个场景最常见的应用是微服务监控拓扑。你用 D2 描述服务之间的调用关系然后每隔几秒拉取一次服务健康状态动态修改每个节点的填充颜色绿色表示正常橙色表示正在降级红色表示故障。这样监控大屏上展示的不再是一堆数字而是一张一眼就能看出问题的动态拓扑图。我做过一个类似的原型后端定时推送 JSON 数据流前端接收数据后更新 SVG 中节点的style图结构保持不变只有颜色和 tooltip 信息在变。整个实现的复杂度并不高但实用性提升非常大——架构图从「设计文档」变成了「运维工具」。这个套路的适用范围也不仅限于监控。凡是图的结构相对稳定、节点状态频繁变化、需要快速响应的场景都可以用这个模式。比如任务编排系统的 DAG 执行状态图、发布流程的管道进度图都是同一个思路。6.3 画布型工具的二次开发边界如果你的需求不是「数据驱动」而是「团队协作画图」那画布型工具依然是不可替代的。Excalidraw 和 tldraw 这类开源画布工具都提供了组件库级别的 SDK你可以把它们嵌入自己的应用里定制默认图形、禁用不需要的功能、接入自己的数据源。以 tldraw 为例它的自定义能力非常强。你可以自定义 shape 类型比如创建一个「云资源」节点绑定自己的数据属性然后通过它的 store 和 persistence API 把画布数据持久化到自己的后端。Excalidraw 也有类似的能力只是自定义深度略低一些但胜在 API 简单嵌入成本低。但这里我要给出一个明确的边界画布型工具适合做「人参与」的图不适合做「机器生成」的图。如果你的图需要根据代码动态生成、随数据变化自动更新画布型工具是错误的选择反过来如果你需要的是白板讨论、自由表达、高度定制视觉代码型工具也会很痛苦。做 diagram-design 不是用一个工具取代所有工具而是让每个人、每个场景都能找到最合适的表达方案。回到开头那个观点——图表的本质是信息排序。工具只是辅助你排序的手段真正影响图的质量的是你对关系的理解深度、对信息冗余的删减能力以及你能否把图当作一种长期演进、可被复用的资产来管理。把这些事想清楚了用什么工具画图已经不重要。而如果你的团队还停留在「每个人用不同的软件画一堆没法维护的图」的阶段我建议你从今天开始把正式的架构图、拓扑图、流程图全部迁到文本型工具上来。改一次需求重排一次位置你会发现原来画图也可以像写代码一样干净利落。
返回列表