☰
diagram-design:用代码化设计系统打造专业图表
2026/10/18 8:42:45 网站建设 项目流程

1. 从一张草图到一套系统:diagram-design 到底在解决什么问题

第一次看到 diagram-design 这个词,很多人会以为它只是“画图”的另一种说法。但真正在项目里被图表折磨过的人知道,画图本身从来不是难点,难的是让图表具备设计感、可维护性和表达力。diagram-design 这个项目标题背后,指向的是一套完整的图表设计方法论与工具链实践——它要解决的核心问题是:如何让技术图表、流程示意、架构图、数据关系图不再停留在“能看”的层面,而是达到“好看、好改、好复用”的专业水准。

我在过去几年里参与过不少需要大量出图的项目,从系统架构梳理到业务流程建模,从数据管道可视化到产品原型中的信息图设计。最开始大家的做法都很朴素:打开某个绘图工具,拖几个框,连几条线,导出 PNG 贴进文档。结果就是每次需求一变,图就得重画;不同人画的图风格不统一;图里的文字和配色跟整体设计语言完全脱节。diagram-design 要做的,就是把这种“手工作坊式”的出图方式,升级成一套有规范、有工具、有流程的工程化实践。

这个内容适合谁?如果你是技术文档写作者、产品经理、系统架构师、独立开发者,或者任何需要频繁输出图表的人,diagram-design 的思路和工具组合都能直接拿来用。它不要求你是设计师出身,但会让你理解为什么有些图看起来就是“专业”,而有些图总差一口气。接下来的内容,我会从整体设计思路、核心工具选型、实操流程、常见问题四个维度展开,把我在实际项目中踩过的坑和总结的技巧都摊开来讲。

2. 整体设计思路与方案选型:为什么不用传统绘图工具

2.1 传统绘图工具的三大痛点

在讲 diagram-design 的具体方案之前,先说说为什么很多人最终放弃了传统绘图工具。我用过市面上绝大多数主流绘图软件,它们的问题集中在三个地方。

第一是版本管理灾难。传统绘图工具产出的文件格式往往是二进制或私有格式,Git 无法有效追踪变化。你改了图里的一个标签,提交到版本库后,diff 显示的就是“文件已更改”,根本看不出改了什么。团队协作时,两个人同时改一张图,合并冲突几乎无法解决。

第二是样式复用困难。你精心调好了一套配色和字体,想应用到下一张图,发现只能手动复制样式,或者依赖工具本身的“主题”功能——而主题功能往往限制多多,改一个全局颜色可能把不该变的地方也变了。

第三是与开发流程脱节。技术项目的文档、代码、图表本应是一体的,但传统工具产出的图片是“死”的,无法嵌入 CI/CD 流程,无法自动生成,无法根据数据动态更新。每次数据变了,图就得手动重画。

2.2 diagram-design 的核心思路:图表即代码

diagram-design 的核心思路可以用一句话概括:把图表当作代码来管理和设计。这意味着图表的定义、样式、布局都以文本形式存在,可以被版本控制、被代码生成、被自动化流程处理。

这个思路并不是凭空来的。在软件工程领域,已经有成熟的“Diagrams as Code”实践,比如用特定 DSL 描述架构图、用 Graphviz 定义关系图。但 diagram-design 更进一步,它不只关注“画出来”,还关注“设计好”。也就是说,它把设计系统的那套理念——色彩规范、字体层级、间距规则、组件复用——引入到图表领域。

为什么这个思路更优?因为一旦图表变成文本,你就获得了几个关键能力:可以用 Git 管理每一次修改;可以用变量和模板批量生成风格一致的图;可以把图表生成嵌入构建流程,数据一变图就自动更新;还可以让不同角色的人用自己熟悉的工具参与进来——设计师调样式,工程师写逻辑,文档作者填内容。

2.3 工具选型:为什么是这套组合

在实际落地 diagram-design 时,工具选型决定了你能不能真正把“图表即代码”跑通。我试过多种组合,最终稳定下来的方案是:Mermaid 负责快速表达逻辑关系,D2 负责复杂布局和精细样式,SVG 作为最终输出格式,配合设计令牌(Design Tokens)统一视觉语言。

Mermaid 的优势在于上手极快,语法接近自然语言,适合流程图、时序图、状态图这类逻辑清晰的图表。它的缺点是样式定制能力有限,复杂布局容易失控。所以我把 Mermaid 定位为“草稿和逻辑验证工具”——先用它把关系理清楚,确认无误后再进入精细设计阶段。

D2 是我近两年重点使用的工具。它的语法比 Mermaid 更灵活,支持容器、嵌套、样式覆盖、主题继承,而且布局引擎可以选择 Dagre 或 ELK,对复杂架构图的支持好很多。D2 还支持用变量和类来复用样式,这正好契合 diagram-design 的设计系统理念。

SVG 作为输出格式,是因为它矢量、可编辑、可嵌入网页、可被 CSS 控制。最终产出的图表不是一张死图,而是一个可以被进一步加工的设计资产。

设计令牌是一组预定义的变量,比如--color-primary、--font-heading、--spacing-md。把这些令牌注入到图表生成流程中,就能保证所有图表跟产品的整体设计语言一致。这个做法借鉴了前端设计系统的思路,但在图表领域同样有效。

注意:工具选型没有绝对标准。如果你的团队已经在用某套工具链,优先考虑如何把 diagram-design 的理念嵌入现有流程,而不是推翻重来。工具是手段,一致性和可维护性才是目的。

3. 核心细节解析与实操要点:从语法到样式的完整拆解

3.1 图表结构设计:先想清楚“关系”再动手

很多人画图的第一步是打开工具,然后一边拖框一边想布局。这是效率最低的做法。diagram-design 要求你先用文本描述清楚图表要表达的关系,再考虑视觉呈现。

以一张典型的系统架构图为例。你需要先回答几个问题:系统有哪些组件?组件之间的调用关系是什么?哪些是同步调用,哪些是异步消息?数据流向是单向还是双向?有没有外部依赖?这些问题的答案决定了图表的逻辑结构,而逻辑结构决定了后续的布局和样式选择。

我习惯先用 Mermaid 写一版最简描述:

graph LR A[客户端] --> B[API 网关] B --> C[用户服务] B --> D[订单服务] C --> E[(用户数据库)] D --> F[(订单数据库)] D --> G[消息队列] G --> H[通知服务]

这一版不需要任何样式,只验证逻辑是否正确。确认无误后,再把它翻译成 D2 语法,加入容器、样式和布局控制。

3.2 样式系统设计:让每张图都说同一种视觉语言

diagram-design 最核心的价值体现在样式系统上。没有样式系统的图表,每张图都是孤立的;有了样式系统,所有图表就像同一个产品里的不同页面,视觉上高度统一。

样式系统的第一层是色彩。我通常定义一组语义化的颜色变量:主色用于核心组件,辅助色用于支撑组件,警告色用于异常路径,中性色用于背景和边框。这些颜色不直接写死,而是通过设计令牌注入。

第二层是字体层级。图表里的文字分几个层级:图表标题、模块标题、组件名称、注释说明。每个层级对应不同的字号、字重和颜色。D2 支持通过classes来批量应用这些样式。

第三层是间距和布局规则。组件之间的水平间距、垂直间距、容器内边距,都应该遵循同一套间距系统。D2 的布局引擎会自动处理大部分间距,但你可以通过grid-columns、grid-rows等参数进行微调。

第四层是形状语义。圆形代表什么,矩形代表什么,菱形代表什么,圆角矩形代表什么——这些约定一旦确定,就要在所有图表中保持一致。比如我用圆角矩形表示服务,用圆柱体表示数据库,用平行四边形表示消息队列,用虚线框表示外部系统。

3.3 实操要点:D2 样式配置的常见陷阱

D2 的样式系统很强大,但有几个地方容易踩坑。

第一个坑是样式继承的优先级。D2 允许在全局、容器、节点三个层级定义样式,优先级从低到高。如果你在全局定义了style.fill: "#f0f0f0",在某个容器里又定义了不同的填充色,容器内的节点会继承容器的样式,而不是全局的。这个行为跟 CSS 的继承规则类似,但很多人第一次用会困惑。

第二个坑是类名的作用域。D2 的classes可以定义一组样式,然后应用到多个节点。但类名的作用域是全局的,如果你在多个图表文件中使用了同一个类名但定义了不同的样式,合并时会产生冲突。我的做法是给类名加前缀,比如arch-service、flow-decision,避免命名冲突。

第三个坑是布局引擎的选择。D2 默认使用 Dagre 布局,适合大多数有向图。但如果你的图表层级很深,或者有很多交叉连线,Dagre 的布局可能会很乱。这时候可以切换到 ELK 布局引擎,它对复杂层次结构的处理更好。切换方式是在 D2 文件开头加上direction: right和layout-engine: elk。

提示:D2 的样式调试建议在本地用d2 --watch模式实时预览。每次保存文件,浏览器里的图表会自动刷新,比反复导出图片效率高得多。

3.4 设计令牌的注入方式

设计令牌要真正发挥作用,必须能自动注入到图表生成流程中。我的做法是维护一个tokens.json文件,里面定义所有颜色、字体、间距变量。然后在生成图表前,用一个简单的脚本把令牌转换成 D2 的样式定义,拼接到 D2 文件头部。

import json with open('tokens.json') as f: tokens = json.load(f) style_header = f""" vars: {{ d2-config: {{ theme-id: 0 }} }} classes: {{ primary-service: {{ style: {{ fill: "{tokens['color']['primary-light']}" stroke: "{tokens['color']['primary']}" font-color: "{tokens['color']['text-on-light']}" border-radius: 8 }} }} }} """ with open('diagram.d2') as f: content = f.read() with open('diagram-with-tokens.d2', 'w') as f: f.write(style_header + content)

这个脚本很粗糙,但思路是清晰的:令牌是唯一真相来源,图表样式从令牌派生。改一个颜色,所有图表重新生成后自动更新。

4. 实操过程与核心环节实现:一张架构图的完整诞生记录

4.1 需求分析与信息收集

假设我们要为某个模拟项目绘制一张系统架构图。第一步不是打开工具,而是收集信息。我会找相关开发者聊清楚几个问题:系统有哪些核心模块?模块之间的调用协议是什么?有没有同步和异步的区分?数据存储用了哪些组件?外部依赖有哪些?

这个阶段我通常用纯文本记录,格式不限,关键是信息完整。比如:

  • 客户端通过 HTTPS 调用 API 网关
  • API 网关根据路由转发到用户服务、订单服务、商品服务
  • 用户服务和订单服务各自有独立的数据库
  • 订单服务在创建订单后向消息队列发送事件
  • 通知服务消费消息队列的事件,发送邮件和短信
  • 所有服务都依赖配置中心和日志服务

4.2 逻辑结构验证

把上面的信息翻译成 Mermaid 语法,快速验证逻辑是否完整、关系是否正确:

graph TB Client[客户端] --> Gateway[API 网关] Gateway --> UserSvc[用户服务] Gateway --> OrderSvc[订单服务] Gateway --> ProductSvc[商品服务] UserSvc --> UserDB[(用户数据库)] OrderSvc --> OrderDB[(订单数据库)] ProductSvc --> ProductDB[(商品数据库)] OrderSvc --> MQ[消息队列] MQ --> NotifySvc[通知服务] NotifySvc --> Email[邮件服务] NotifySvc --> SMS[短信服务] UserSvc --> Config[配置中心] OrderSvc --> Config ProductSvc --> Config UserSvc --> Log[日志服务] OrderSvc --> Log ProductSvc --> Log

这一版图能看出基本关系,但有几个问题:配置中心和日志服务的连线太多,显得杂乱;邮件和短信服务可以合并为一个外部依赖;客户端和网关之间应该标注协议。这些问题在逻辑阶段就要发现,而不是等到样式阶段再改。

4.3 翻译为 D2 并加入设计系统

确认逻辑后,把 Mermaid 翻译成 D2,同时引入设计令牌和样式类。以下是核心片段:

direction: right classes: { service: { style: { fill: "#e8f0fe" stroke: "#1a73e8" font-color: "#1a1a1a" border-radius: 8 shadow: true } } database: { style: { fill: "#fce8e6" stroke: "#d93025" font-color: "#1a1a1a" border-radius: 4 } } external: { style: { fill: "#f1f3f4" stroke: "#5f6368" font-color: "#5f6368" border-radius: 20 stroke-dash: 4 } } } client: 客户端 { class: external } gateway: API 网关 { class: service } user_svc: 用户服务 { class: service } order_svc: 订单服务 { class: service } product_svc: 商品服务 { class: service } user_db: 用户数据库 { class: database; shape: cylinder } order_db: 订单数据库 { class: database; shape: cylinder } product_db: 商品数据库 { class: database; shape: cylinder } mq: 消息队列 { class: service; shape: parallelogram } notify_svc: 通知服务 { class: service } infra: 基础设施 { config: 配置中心 { class: service } log: 日志服务 { class: service } } client -> gateway: HTTPS gateway -> user_svc gateway -> order_svc gateway -> product_svc user_svc -> user_db order_svc -> order_db product_svc -> product_db order_svc -> mq: 发布事件 mq -> notify_svc: 消费事件 user_svc -> infra.config order_svc -> infra.config product_svc -> infra.config user_svc -> infra.log order_svc -> infra.log product_svc -> infra.log

4.4 布局调优与视觉打磨

D2 默认的 Dagre 布局在这个场景下会把基础设施放在底部,导致连线交叉严重。我做了几个调整:把基础设施容器设置为direction: right,让配置中心和日志服务并排;把消息队列和通知服务放在订单服务右侧,形成清晰的数据流;给客户端到网关的连线加上协议标注。

调优后的布局逻辑是:左侧是客户端和网关,中间是三个核心服务及其数据库,右侧是消息队列和通知服务,底部是基础设施。这样数据流向从左到右,符合阅读习惯。

视觉打磨还包括:给核心服务加上阴影,让它们从背景中浮起来;数据库用圆柱体形状,一眼就能识别;外部系统用虚线边框,跟内部服务区分开;连线上的标签用较小字号和灰色,不抢主体内容的注意力。

4.5 导出与嵌入文档

最终用d2 --format svg导出 SVG 文件。SVG 的好处是可以直接嵌入 Markdown 文档,也可以嵌入网页。如果文档平台支持 HTML,还可以给 SVG 加上交互效果,比如鼠标悬停显示组件详情。

导出命令:

d2 --format svg --theme 0 --pad 20 architecture.d2 architecture.svg

--theme 0使用默认主题,--pad 20给图表周围留出 20 像素的内边距,避免内容贴边。

实操心得:SVG 导出后,建议用浏览器打开检查一遍。有时候 D2 的布局引擎会在某些分辨率下产生文字重叠,提前发现比发布后再改成本低得多。

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

5.1 图表文字重叠或溢出

这是 D2 和 Mermaid 都会遇到的问题。原因通常是节点文字太长,或者布局引擎没有正确计算节点尺寸。解决方法有几个:缩短节点标签,用换行符\n手动断行;调整grid-columns或grid-rows参数;切换到 ELK 布局引擎;或者给节点设置固定的width和height。

我遇到过一个案例:某个服务的名称特别长,D2 自动计算的节点宽度不够,文字溢出了边框。后来在节点定义里加了width: 200,问题解决。但更好的做法是给这类长名称的节点定义一个专门的样式类,统一设置宽度。

5.2 连线交叉严重

连线交叉是复杂架构图的常见问题。D2 的 Dagre 引擎在有向无环图上表现不错,但遇到多层级、多交叉的场景就力不从心。我的经验是:先尝试调整direction参数,把right改成down或者反过来;如果还不行,把一些节点分组到容器里,减少跨容器的连线;最后才考虑换布局引擎。

还有一个技巧是虚拟节点。在两个距离很远但需要连线的节点之间插入一个不可见的虚拟节点,让连线经过它,可以引导布局引擎产生更合理的路径。D2 支持用style.opacity: 0来隐藏节点。

5.3 样式不生效或冲突

样式不生效通常有三个原因:类名拼写错误、样式优先级被覆盖、或者样式属性名写错了。D2 的样式属性名跟 CSS 不完全一样,比如边框圆角是border-radius,阴影是shadow,虚线是stroke-dash。建议对照官方文档逐一核对。

样式冲突则多发生在多个图表文件合并时。如果两个文件都定义了service类但样式不同,后加载的会覆盖先加载的。解决办法是给类名加项目前缀,或者把公共样式抽到一个单独的styles.d2文件里,所有图表都引用这个文件。

5.4 导出图片模糊或尺寸不对

导出 PNG 时如果分辨率不够,图片会模糊。D2 导出 PNG 时可以指定--scale参数,比如--scale 3表示三倍分辨率。但更推荐导出 SVG,因为 SVG 是矢量的,无论放大多少倍都清晰。

如果导出后发现图表周围空白太多或太少,调整--pad参数。如果图表整体尺寸不对,检查 D2 文件里有没有设置width和height,这两个参数会覆盖布局引擎的自动计算。

5.5 常见问题速查表

问题现象可能原因排查方法解决方案
文字溢出节点节点宽度不足检查节点标签长度设置width或手动断行
连线交叉严重布局引擎选择不当尝试不同direction切换 ELK 引擎或分组
样式不生效类名拼写错误检查类名和属性名对照文档修正
样式冲突类名重复定义检查多个文件的类名加前缀或抽公共样式
导出图片模糊PNG 分辨率不足检查--scale参数改用 SVG 或提高 scale
图表尺寸异常手动设置了宽高检查width/height移除或调整参数

避坑技巧:每次修改 D2 文件后,先用d2 --watch预览,确认无误再导出。直接导出再打开图片检查,效率低很多。另外,建议把 D2 源文件和导出的 SVG 都提交到版本库,这样既能追踪样式变化,又能保证文档里的图片始终可用。

6. 把 diagram-design 嵌入团队工作流的一些经验

6.1 建立图表规范文档

diagram-design 要落地,光有工具不够,还需要一份团队共识的图表规范。这份规范不需要很长,但必须明确几个核心约定:什么场景用什么类型的图;每种图的标准配色和字体;节点形状的语义;连线的样式规则;图表的命名和存放位置。

我们团队的规范文档只有两页,但覆盖了 90% 的日常出图场景。新成员入职时,先读这份规范,再看几个示例文件,基本就能产出风格一致的图表。

6.2 把图表生成加入构建流程

如果项目已经有 CI/CD 流程,可以把图表生成作为一个构建步骤。每次 D2 源文件变更,自动重新生成 SVG 并部署到文档站点。这样文档里的图表永远是最新的,不需要手动更新。

实现方式很简单:在构建脚本里加一行d2 --format svg src/diagrams/*.d2 dist/diagrams/,然后把dist/diagrams/目录部署到文档服务器。如果用的是静态站点生成器,还可以把 SVG 直接嵌入 Markdown。

6.3 定期回顾和优化样式系统

设计令牌和样式类不是定下来就不变的。随着项目演进,可能会发现某些颜色对比度不够,某些字体大小在移动端看不清,某些形状语义容易混淆。我建议每个季度花半小时回顾一下图表样式,根据实际使用中的反馈做微调。

回顾时重点看几个指标:图表在文档中的实际显示效果;读者有没有反馈看不懂;新出的图表有没有偏离规范;设计令牌有没有跟产品设计系统脱节。这些检查不需要很正式,但坚持做能避免样式系统慢慢腐化。

6.4 处理历史图表的迁移

如果团队之前已经积累了大量用传统工具画的图,全部迁移到 diagram-design 成本很高。我的建议是:新图一律用新流程,旧图在需要修改时顺便迁移。不要为了统一而统一,迁移的目的是降低维护成本,如果一张图半年都不改一次,迁移的优先级就很低。

迁移时也不是完全重画。可以先把旧图导出为 SVG,然后用 D2 的background-image功能把旧图作为背景,在上面叠加新的样式元素。这样既能保留旧图的内容,又能逐步统一视觉风格。

6.5 一些让我少走弯路的工具和资源

D2 官方文档是我查得最多的资源,尤其是样式属性和布局引擎那几页。Mermaid 的在线编辑器适合快速验证逻辑。SVG 优化工具可以压缩导出文件的大小。设计令牌的格式我参考了 W3C 的设计令牌规范草案,虽然还没正式定稿,但思路很清晰。

另外,我强烈建议把常用的 D2 样式片段保存成代码片段,用编辑器的 snippet 功能快速插入。比如一个标准的服务节点定义、一个数据库节点定义、一个外部系统定义,每次新建图表时直接插入,比从头写快得多。

7. 关于图表设计的一些个人体会

做了这么多图表,我越来越觉得 diagram-design 的核心不是工具,而是约束。没有约束的图表工具给了你无限的自由,但自由往往导致混乱。设计令牌是约束,样式类是约束,形状语义是约束,布局规则也是约束。正是这些约束,让不同人、不同时间产出的图表能够保持一致。

另一个体会是:图表的第一读者是你自己。很多人画图时想着“别人要看”,于是堆砌信息,结果图变得复杂难懂。但如果你先为自己画图——用最清晰的方式表达你理解的结构——然后再考虑别人的阅读习惯做调整,图的质量会高很多。

最后,不要追求一步到位。我最早用 D2 画的图也很粗糙,样式混乱,布局奇怪。但每次画完花几分钟回顾一下哪里可以改进,下次就能好一点。图表设计跟写代码一样,是练出来的,不是学出来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询