1. 从一张草图到一套系统:diagram-design 到底在解决什么问题
第一次看到 diagram-design 这个词,很多人会以为它只是“画图”的另一个说法。但真正在项目里被图表折磨过的人知道,画图本身从来不是最痛的部分。最痛的是:图画完了,需求变了;图改完了,风格不统一;风格统一了,换个人接手又全部推倒重来。diagram-design 要处理的,正是这一连串连锁反应。
我把它理解成一套“图表设计系统”,而不是一个单纯的绘图工具。它的核心目标是把图表的结构、样式、语义、复用四件事拆开管理,让画图从一次性劳动变成可维护的资产。你画的不是一张图,而是一套能持续产出图的规则和组件。
这套东西适合谁?如果你只是偶尔画个流程图发群里,可能用不上。但如果你符合下面任意一条,diagram-design 的思路就值得你花时间研究:
- 团队里多个人要画同一类图,风格总是对不齐;
- 图表需要频繁更新,每次改动都像重新画一遍;
- 同一套逻辑要输出成不同形式,比如架构图、时序图、部署图;
- 图表要嵌入文档、代码仓库或演示材料,需要版本管理。
它解决的问题可以概括成一句话:把图表的“设计决策”从“绘制动作”里剥离出来。绘制动作可以交给工具,但设计决策必须被沉淀。这个思路和前端领域把样式抽成设计令牌(Design Token)、把 UI 抽成组件库是同一个逻辑,只是对象换成了图表。
我见过太多项目,图表散落在各个人的本地文件夹里,命名混乱,源文件丢失,最后只剩一张导出的 PNG。diagram-design 的价值就在于,它让图表有了“源文件”和“构建流程”的概念。下面我会从整体设计、核心细节、实操过程、问题排查四个层面,把这套思路完整拆开。
2. 整体设计与思路拆解:为什么要把图表当代码来管
2.1 图表失控的三个根源
在动手设计任何方案之前,得先搞清楚图表为什么会失控。我复盘过多个项目,根源基本逃不出这三条:
第一,样式和内容耦合。画图的时候,你一边想“这个框放哪里”,一边想“用什么颜色、什么字体、多大字号”。这两类决策混在一起,改一个就得动全身。
第二,缺少中间层。从“我想表达什么”直接跳到“最终长什么样”,中间没有一层来描述“这是一个服务节点”“这是一条异步调用”。没有中间层,就无法批量替换和校验。
第三,没有版本概念。图表以二进制或专有格式保存,diff 不可读,合并冲突无法处理,回滚基本靠记忆。
diagram-design 的整体设计就是针对这三条逐一拆解。它把图表拆成四层:语义层、结构层、样式层、渲染层。语义层描述“这是什么”,结构层描述“它和谁相连”,样式层描述“它长什么样”,渲染层负责“输出成什么格式”。这四层分离之后,每一层都可以独立演进。
2.2 为什么选择“声明式”而不是“拖拽式”
拖拽式工具上手快,但它的致命伤是:操作过程不可复现。你拖了一个框到某个位置,这个位置信息只存在于文件里,没有任何逻辑能解释“为什么在这里”。换一个人、换一台机器,结果可能就不一样。
声明式的好处是,图表的每一个元素都由文本描述。文本可以被 diff、被 review、被版本控制。你可以清楚地看到“这次改动把数据库节点从 A 组移到了 B 组”,而不是“某个像素位置变了”。
当然,声明式也有代价。它要求你先把图表的逻辑想清楚,不能边画边想。对于探索性强的草图阶段,拖拽仍然更合适。所以我的建议是:探索阶段用拖拽,定稿阶段转声明式。diagram-design 主要服务于定稿和长期维护阶段。
2.3 样式层为什么要独立成“令牌”
样式层独立成令牌(Token)是这套设计里最关键的一步。所谓令牌,就是把颜色、字号、间距、线宽这些具体值,抽象成有名字的变量。比如不写#2F6FED,而写color-primary;不写14px,而写font-size-body。
这样做的好处有三个。第一,换主题时只改令牌定义,所有图表同步更新。第二,不同图表之间天然保持一致,因为大家引用的是同一套令牌。第三,令牌可以被校验,比如检查是否存在未定义的令牌引用。
我实测下来,一个中等规模的项目,图表样式令牌控制在 20 到 30 个就够用了。太多会导致选择困难,太少又不够表达。下面这张表是我常用的令牌分类:
| 令牌类别 | 示例名称 | 典型值 | 使用场景 |
|---|---|---|---|
| 颜色 | color-primary | #2F6FED | 主流程节点 |
| 颜色 | color-warning | #E8A33D | 异常分支 |
| 字号 | font-size-title | 16px | 节点标题 |
| 字号 | font-size-label | 12px | 连线标签 |
| 间距 | spacing-node | 24px | 节点间距 |
| 线宽 | stroke-default | 1.5px | 普通连线 |
| 线宽 | stroke-emphasis | 2.5px | 关键路径 |
注意:令牌命名要有语义,不要用 color-blue 这种描述外观的名字。因为一旦品牌色从蓝改成绿,color-blue 这个名字就变成了谎言。
2.4 结构层为什么用“节点+边”而不是“图形+位置”
结构层用节点和边来描述,而不是直接描述图形和坐标,这是为了让布局算法接管位置计算。你只需要说“A 连到 B”,不需要说“A 在 (100, 200),B 在 (300, 200)”。
这样做的好处是,当节点数量变化时,布局会自动调整,不会出现重叠或大片空白。代价是你对最终布局的控制力会下降。但对于大多数工程图表来说,可维护性比精确控制更重要。
如果确实需要精确控制,可以在节点上附加“分组”或“层级”信息,让布局算法按组排列。这比直接写坐标要稳健得多。
3. 核心细节解析与实操要点:四层结构怎么落地
3.1 语义层:给每个节点一个“身份”
语义层的核心是给节点定义类型。常见的类型包括:服务、数据库、队列、外部系统、用户、网关。每个类型对应一套默认样式和行为。
为什么要定义类型,而不是直接给每个节点写样式?因为类型可以被校验和统计。比如你可以检查“所有数据库节点是否都标注了存储引擎”,或者统计“这个系统里有多少个外部依赖”。这些在纯样式层面是做不到的。
定义类型时,我建议遵循“最小够用”原则。一开始不要超过 8 种类型,否则维护成本会超过收益。下面是我常用的类型定义示例:
node_types: service: label: 服务 shape: rounded-rect style: color-primary database: label: 数据库 shape: cylinder style: color-storage queue: label: 队列 shape: parallelogram style: color-async external: label: 外部系统 shape: dashed-rect style: color-external提示:shape 和 style 分开定义,是为了让同一个类型在不同主题下可以换样式,但形状保持不变。形状承载语义,样式承载视觉。
3.2 结构层:边的方向与语义
边不只是“连接”,它还有方向和语义。同步调用、异步消息、数据流向、依赖关系,这些都应该在边上体现出来。
我的做法是给边也定义类型,每种类型对应不同的线型和箭头。比如:
- 同步调用:实线 + 实心箭头;
- 异步消息:虚线 + 空心箭头;
- 数据流向:粗实线 + 实心箭头;
- 依赖关系:点线 + 无箭头。
这样做的好处是,读图的人不需要看文字说明,光看线型就能理解关系。这在复杂系统图里尤其重要,因为文字标注往往会被省略或看不清。
边的另一个要点是标签位置。标签应该靠近箭头端还是中间?我的经验是:描述动作的标签放中间,描述条件的标签放箭头端。比如“调用”放中间,“超时则重试”放箭头端。
3.3 样式层:令牌的引用与覆盖
样式层的实操要点是:默认引用令牌,特殊情况才覆盖。大部分节点应该直接使用类型对应的令牌,只有少数需要强调的节点才单独覆盖样式。
覆盖时要遵循“最小覆盖”原则。比如只想改颜色,就不要把字号、线宽一起重写。这样当令牌更新时,未覆盖的部分会自动跟随。
我见过一种反模式:每个节点都写全套样式。结果令牌形同虚设,改一个颜色要改几十处。这是典型的“把令牌当装饰,而不是当机制”。
3.4 渲染层:多格式输出的取舍
渲染层负责把前面三层描述转换成最终输出。常见的输出格式有 SVG、PNG、PDF,以及嵌入文档用的矢量格式。
选择输出格式时,要考虑三个因素:清晰度、可编辑性、文件大小。SVG 清晰度最高、可编辑,但复杂图表文件会很大。PNG 通用性好,但放大后会模糊。PDF 适合打印,但嵌入网页不方便。
我的建议是:源文件用声明式文本,输出优先 SVG,需要位图时再转 PNG。这样既保留了可编辑性,又兼顾了通用性。
| 输出格式 | 清晰度 | 可编辑 | 文件大小 | 推荐场景 |
|---|---|---|---|---|
| SVG | 矢量 | 是 | 中 | 文档、网页 |
| PNG | 位图 | 否 | 小 | 聊天、演示 |
| 矢量 | 部分 | 中 | 打印、归档 |
4. 实操过程与核心环节实现:从零搭一套图表系统
4.1 环境准备与目录结构
先说明一点,diagram-design 不是某个具体软件的名字,而是一套设计思路。你可以用不同的工具来实现它。我这里以文本描述加渲染工具的组合为例,讲一套可复现的流程。
目录结构建议这样组织:
diagram-design/ tokens/ colors.yaml typography.yaml spacing.yaml types/ node-types.yaml edge-types.yaml diagrams/ system-overview.yaml >colors: primary: "#2F6FED" primary-light: "#DCE7FF" storage: "#3FA66A" async: "#E8A33D" external: "#9B59B6" text: "#1F2933" text-muted: "#6B7280" line: "#9AA5B1"字号和间距类似。定义完之后,要在校验脚本里检查:所有图表引用的令牌是否都存在。这个检查能避免大量“渲染出来是黑色”的问题。
4.3 图表描述与渲染
图表描述文件只写结构和引用。比如:
id: system-overview title: 系统总览 nodes: - id: user type: external label: 用户 - id: gateway type: service label: 网关 - id: order type: service label: 订单服务 - id: db type: database label: 订单库 edges: - from: user to: gateway type: sync label: 请求 - from: gateway to: order type: sync label: 转发 - from: order to: db type: data label: 读写渲染脚本读取令牌、类型和图表描述,生成 SVG。这个过程可以集成到持续集成流程里,每次图表描述变更就自动重新渲染并检查。
4.4 参数计算:间距和字号怎么定
间距和字号不是随便定的,它们之间有比例关系。我常用的比例是:字号 : 行高 : 节点内边距 : 节点间距 = 1 : 1.5 : 1 : 2。
假设正文字号是 14px,那么行高 21px,节点内边距 14px,节点间距 28px。这个比例能让图表看起来不拥挤也不松散。
字号层级建议不超过四级:标题 16px、正文 14px、标签 12px、注释 10px。超过四级会让读者难以区分主次。
注意:字号不要小于 10px,否则在投影或打印时会看不清。如果内容太多,宁可拆成两张图,也不要缩小字号。
4.5 实操现场记录:一次完整的图表迭代
我记录过一次典型的迭代过程。需求是给系统总览图增加一个缓存节点。
第一步,在图表描述里加节点:
- id: cache type: database label: 缓存第二步,加边:
- from: order to: cache type: data label: 查询第三步,运行校验脚本,检查令牌引用和类型定义是否完整。第四步,运行渲染脚本,生成 SVG。第五步,人工检查布局是否合理,必要时调整节点顺序。
整个过程不到五分钟,而且改动是可追溯的。如果换成拖拽工具,加一个节点可能要重新调整周围所有元素的位置。
5. 常见问题与排查技巧实录
5.1 渲染出来全是黑色怎么办
这是最常见的问题,原因通常是令牌引用失败。排查顺序是:先检查令牌名称拼写,再检查令牌文件是否被正确加载,最后检查渲染工具是否支持该令牌格式。
我的经验是,在校验脚本里加一条“未定义令牌检查”,能在渲染前就发现问题,比渲染后肉眼排查快得多。
5.2 布局重叠怎么处理
布局重叠通常是因为节点太多或分组不合理。解决办法有三个:一是增加分组信息,让布局算法按组排列;二是调整节点顺序,把关联紧密的节点放在一起;三是拆图,把一张大图拆成多张小图。
我倾向于优先拆图。一张图超过 15 个节点,可读性就会明显下降。与其在一张图里挤,不如拆成“总览图 + 分模块图”。
5.3 样式不一致怎么排查
样式不一致往往是因为有人绕过了令牌,直接写了具体值。排查方法是搜索图表描述文件里的颜色值和字号值,看是否有硬编码。
更彻底的办法是在校验脚本里禁止硬编码,只允许令牌引用。这样从机制上杜绝了不一致。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方式 |
|---|---|---|---|
| 渲染全黑 | 令牌未定义 | 检查令牌名称 | 补定义或改引用 |
| 布局重叠 | 节点过多 | 统计节点数 | 拆图或分组 |
| 样式不一致 | 硬编码样式 | 搜索颜色值 | 改用令牌引用 |
| 边方向错误 | 类型定义错 | 检查边类型 | 修正类型定义 |
| 输出模糊 | 用了位图 | 检查输出格式 | 改用 SVG |
5.5 独家避坑技巧
第一个技巧:先定令牌,再画图。很多人反过来,先画图再抽令牌,结果抽出来的令牌乱七八糟。正确的顺序是先定义一套最小令牌,再开始画。
第二个技巧:图表描述文件要 review。不要觉得图表是“附属品”就不 review。图表描述里的结构错误,往往反映了对系统理解的错误。
第三个技巧:保留渲染脚本。不要手动导出图片,要用脚本渲染。这样任何人拿到仓库都能重新生成一模一样的图。
第四个技巧:给图表加版本号。图表描述文件里加一个 version 字段,方便追溯“这张图是什么时候的”。
6. 工具选型与扩展思路
6.1 文本描述工具怎么选
文本描述图表的工具不少,选择时重点看三点:是否支持自定义类型、是否支持样式令牌、是否支持多格式输出。这三点决定了它能不能支撑 diagram-design 的思路。
如果工具只支持固定语法,不支持自定义类型,那语义层就落不了地。如果样式只能内联写,令牌层就形同虚设。
6.2 与文档系统的集成
图表最终要嵌入文档。集成方式有两种:一是渲染成 SVG 后嵌入,二是文档系统直接支持图表描述语法。
前者通用性好,后者维护性更好。我的建议是,如果文档系统支持,优先用后者,因为图表描述和文档内容可以一起版本管理。
6.3 后续可以扩展的方向
这套思路可以往几个方向扩展。一是自动布局优化,根据图的拓扑结构自动选择最佳布局算法。二是图表校验规则,比如检查是否存在孤立节点、是否存在循环依赖。三是多主题输出,同一套图表描述输出亮色和暗色两个版本。
我个人在实际操作中的体会是,diagram-design 最大的价值不是让你画得更快,而是让你画得更稳。快是暂时的,稳是长期的。一套好的图表系统,应该让新人也能画出风格一致的图,让改动变得可预期,让图表真正成为项目资产而不是一次性消耗品。
最后再分享一个小技巧:如果你的团队刚开始尝试这套方法,不要一上来就搞全套。先从颜色令牌开始,只统一颜色。等大家习惯了,再加字号和间距。循序渐进,比一步到位更容易落地。