1. 从一张草图到一套系统:diagram-design 到底在解决什么问题
第一次听到 “diagram-design” 这个词,很多人会下意识觉得它只是“画图”的另一种说法。但真正在项目里被图表折磨过的人都知道,画图本身从来不是难点,难的是让图表具备可维护性、可复用性和可协作性。diagram-design 这个项目标题背后指向的,其实是一整套围绕“图表即设计资产”的工作方法——它要解决的核心问题是:当一张架构图、流程图或时序图被画出来之后,它能不能像代码一样被版本管理、被团队复用、被自动化生成,而不是变成一张躺在文档里、三个月后没人敢改的 PNG。
我做过不少和图表相关的项目,从早期的纯手工拖拽,到后来用 DSL 描述再渲染,中间踩过的坑基本能写一本书。diagram-design 这个方向之所以值得单独拿出来聊,是因为它把“设计”和“图表”这两个原本分属不同工种的词捏在了一起。传统流程里,产品经理画原型、架构师画架构图、开发写文档,三拨人用三套工具,最后拼在一起风格不统一、信息还经常对不上。diagram-design 的思路是:把图表当成设计系统的一部分来对待,定义好颜色、字体、间距、节点样式、连线规则,然后让所有图表都从这套规则里长出来。
这套方法适合谁?如果你只是偶尔画一张流程图发群里,那用现成工具拖两下就够了,没必要上这套。但如果你所在的团队有超过三个人在维护同一份技术文档,或者你的项目需要频繁更新架构图、数据流图、状态机图,那 diagram-design 的价值就会立刻显现出来。它能让“改一张图”从半小时的体力活变成改几行配置的脑力活,这个差距在长期项目里是数量级的。
我个人的判断是,diagram-design 本质上属于“图表工程化”这个更大的范畴。它借鉴了前端设计系统(Design System)的思路,把图表拆解成 token(设计变量)、component(节点与连线组件)、template(图表模板)三层。理解了这三层,你就理解了整个项目的骨架。接下来我会从设计思路、核心细节、实操落地、问题排查四个维度,把这套东西掰开揉碎讲清楚,尽量让没接触过的人也能照着搭起来。
2. 整体设计思路:为什么图表需要一套“设计系统”
2.1 从“画图”到“定义图”:思维方式的转变
大多数人画图的过程是这样的:打开工具,拖一个矩形,写上文字,再拖一个箭头连起来,调整颜色和大小,导出。这个过程的问题在于,每一次画图都是一次从零开始的决策。这个矩形用多大圆角?箭头用实线还是虚线?颜色用蓝的还是灰的?这些决策在单张图里无所谓,但当你有了二十张图,每张图的决策都不一样时,整个文档看起来就像拼凑出来的。
diagram-design 的第一个核心思路,就是把这些“每次都要重新决策”的东西提前固化下来。具体做法是定义一套design token,也就是设计变量。比如:
color-node-primary: 主节点填充色color-node-border: 节点边框色color-edge-default: 默认连线色font-family-diagram: 图表统一字体spacing-node-padding: 节点内边距radius-node: 节点圆角半径
这些 token 一旦定下来,所有图表都引用它们,而不是写死具体值。这样做的好处是,当你想把整套图的主题从“蓝色系”换成“绿色系”时,只需要改一个 token 的值,所有图自动更新。我试过在一个有三十多张图的项目里做主题切换,改 token 花了不到五分钟,如果手工改图,至少两天。
提示:token 的命名要有语义,不要用
blue-500这种描述颜色的名字,而要用node-primary这种描述用途的名字。颜色会变,用途不会。
2.2 三层架构:token、component、template
diagram-design 的架构可以拆成三层,每一层解决不同粒度的问题。
第一层是 token 层,负责定义所有原子级的视觉属性。这一层的关键是“少而精”,不要定义几百个 token,那样反而没人记得住。我的经验是,一个图表设计系统里,token 数量控制在 20 到 40 个之间比较合适,覆盖颜色、字体、间距、圆角、线宽、阴影这几类就够了。
第二层是 component 层,也就是节点和连线的组件化。比如“服务节点”“数据库节点”“队列节点”“外部系统节点”,每种节点有固定的形状、图标位置、文字排版规则。连线也分“同步调用”“异步消息”“数据流”等类型,每种类型有固定的线型和箭头样式。这一层解决的是“同类元素长得一样”的问题。
第三层是 template 层,也就是整张图的模板。比如“微服务架构图模板”“时序图模板”“状态机模板”,模板里预置了常见的布局结构和示例节点,新建图的时候直接套模板,改改文字就能用。这一层解决的是“同类图结构一致”的问题。
这三层的关系是:template 引用 component,component 引用 token。改 token 影响所有 component,改 component 影响所有引用它的 template。这种级联关系是整套系统可维护性的来源。
2.3 为什么不用现成的绘图工具直接画
有人会问,现成的绘图工具已经很好用了,为什么还要折腾这套?我的回答是:现成工具解决的是“画出来”,diagram-design 解决的是“管起来”。当你只有一张图时,两者没区别;当你有五十张图、五个维护者、每周都要更新时,区别就大了。
具体来说,手工画图有三个绕不开的痛点。第一是一致性无法保证,不同人画的图风格不同,甚至同一个人不同时间画的图也不一样。第二是批量修改成本高,改一个全局样式要一张张图去调。第三是无法自动化,比如你想根据代码里的服务依赖关系自动生成架构图,手工画图完全做不到。
diagram-design 通过“描述式定义 + 渲染”的方式,把图表变成了可以程序化处理的对象。你可以用 YAML、JSON 或者专门的 DSL 来描述一张图,然后用渲染器把它变成 SVG 或 PNG。描述文件可以进版本库,可以 diff,可以 code review,可以 CI 自动生成。这才是它真正的价值所在。
3. 核心细节解析:token、组件与布局的实操要点
3.1 Design Token 的定义与组织方式
Token 的定义方式直接决定了整套系统的可维护性。我见过两种常见的组织方式,各有适用场景。
第一种是扁平式,所有 token 放在一个文件里,用命名前缀区分类别:
color: node-primary: "#4A90D9" node-secondary: "#7B68EE" node-danger: "#E74C3C" edge-default: "#666666" edge-dashed: "#999999" font: family: "Inter, sans-serif" size-node: 14 size-label: 12 spacing: node-padding: 12 node-gap: 40 radius: node: 8 badge: 4第二种是分层式,先定义基础色板,再定义语义 token 引用基础色板:
palette: blue-500: "#4A90D9" blue-600: "#3A7BC8" red-500: "#E74C3C" semantic: node-primary-bg: "{palette.blue-500}" node-primary-border: "{palette.blue-600}" node-danger-bg: "{palette.red-500}"我个人的建议是,中小项目用扁平式就够了,大项目或者需要多主题切换的用分层式。分层式的好处是换主题时只改 palette 层,semantic 层不动。但分层式也带来了一层间接性,新人理解成本更高。不要为了“看起来专业”而过度设计。
注意:token 的值一旦定下来,就不要在图表描述文件里写死具体颜色值。我踩过的坑是,早期有几张图直接写了
#4A90D9,后来换主题时这几张图没跟着变,排查了半天才发现是硬编码。
3.2 节点组件的设计规范
节点是图表里出现频率最高的元素,节点组件的设计质量直接决定整张图的可读性。一个完整的节点组件需要定义以下属性:
| 属性 | 说明 | 常见取值 |
|---|---|---|
| 形状 | 节点的外轮廓 | 矩形、圆角矩形、圆形、菱形、圆柱 |
| 尺寸 | 宽高范围 | 最小宽 120px,高度自适应 |
| 内边距 | 文字与边框的距离 | 水平 12px,垂直 8px |
| 字体 | 节点内文字样式 | 14px,中等字重 |
| 图标 | 可选的类型图标 | 左上角或左侧居中 |
| 状态色 | 不同状态的填充色 | 正常、警告、错误、禁用 |
这里有个容易被忽略的细节:节点的宽度应该由内容决定,而不是固定值。早期我为了对齐好看,把所有节点设成固定宽度,结果遇到长名字就被截断,遇到短名字就留一大片空白。后来改成“最小宽度 + 内容自适应”,配合布局算法自动对齐,效果好很多。
另一个细节是图标的位置。图标放在节点左侧居中是最常见的做法,但如果节点文字是多行的,图标居左会导致视觉重心偏移。我的做法是图标固定在左上角,文字从图标右侧开始排版,这样多行文字也能保持整齐。
3.3 连线类型的语义化定义
连线不是简单的“两点之间画条线”,不同类型的连线承载不同的语义。在 diagram-design 里,连线类型应该和业务语义绑定,而不是和视觉样式绑定。
我常用的连线类型定义如下:
- 同步调用:实线 + 实心箭头,表示请求-响应模式
- 异步消息:虚线 + 空心箭头,表示消息队列或事件
- 数据流:粗实线 + 实心箭头,表示数据批量流动
- 依赖关系:点线 + 无箭头,表示编译期或部署期依赖
- 继承/实现:虚线 + 空心三角箭头,用于类图
这样定义的好处是,画图的人只需要选择“这是一条异步消息”,样式自动应用,不需要每次去调线型和箭头。而且当团队约定“异步消息统一用虚线”之后,任何人看到虚线就知道是异步,不需要看图例。
提示:连线尽量避免交叉。如果布局算法无法避免交叉,可以考虑用“跳线”样式(交叉处画一个小弧),但跳线本身也会增加视觉噪音,能不用就不用。
3.4 布局算法的选择与参数调优
布局是图表设计里最容易被低估的环节。手工画图时,布局靠人眼判断,差不多就行。但 diagram-design 追求的是自动化布局,这就需要算法介入。
常见的布局算法有几种。层次布局适合有向无环图,比如流程图、依赖图,它会把节点按层级排列,边从上往下或从左往右。力导向布局适合关系复杂的网络图,节点之间相互排斥、边像弹簧一样拉扯,最终达到平衡。网格布局适合规则排列的场景,比如部署图。
我实际用得最多的是层次布局,因为大部分技术图表都是有向的。层次布局有几个关键参数需要调:
rankdir:布局方向,TB(上到下)或 LR(左到右)nodesep:同层节点之间的间距,太小会挤,太大会散ranksep:层与层之间的间距,一般比 nodesep 大edgesep:边与边之间的最小间距,防止重叠
我的经验值是:nodesep取 40 到 60,ranksep取 60 到 80,edgesep取 10 到 20。具体值要根据图的规模和节点大小微调。图越大,间距要适当放大,否则会显得很密。
4. 实操过程:从零搭建一套 diagram-design 工作流
4.1 工具选型:描述语言与渲染器的搭配
搭建 diagram-design 工作流,第一步是选工具。核心是两个东西:描述语言和渲染器。
描述语言方面,常见的选择有 Mermaid、Graphviz DOT、PlantUML、D2 等。它们各有特点:
| 工具 | 语法风格 | 优势 | 局限 |
|---|---|---|---|
| Mermaid | 类 Markdown | 上手快,集成广 | 样式定制能力弱 |
| Graphviz DOT | 声明式 | 布局算法强 | 语法偏底层 |
| PlantUML | 类自然语言 | 图类型丰富 | 渲染依赖 Java |
| D2 | 现代声明式 | 样式与布局分离好 | 生态相对新 |
如果你追求快速上手,Mermaid 是最省事的,很多文档平台原生支持。但如果你要做一套完整的 design system,Mermaid 的样式定制能力会很快成为瓶颈。我个人的选择是D2 或 Graphviz 做核心渲染,Mermaid 做轻量场景。D2 的设计理念和 diagram-design 很契合,它把样式、布局、主题都做成了可配置的层。
渲染器方面,大部分工具都提供 CLI 和库两种形式。CLI 适合 CI 集成,库适合嵌入到自己的应用里。我一般会同时用两种:本地开发用 CLI 快速预览,生产环境用库做动态渲染。
4.2 目录结构设计:让图表资产可管理
工具选好之后,接下来是目录结构。一个好的目录结构能让图表资产“可发现、可复用、可追溯”。我常用的结构是这样的:
diagram-design/ ├── tokens/ │ ├── palette.yaml │ ├── semantic.yaml │ └── typography.yaml ├── components/ │ ├── nodes/ │ │ ├── service.yaml │ │ ├── database.yaml │ │ └── queue.yaml │ └── edges/ │ ├── sync.yaml │ └── async.yaml ├── templates/ │ ├── microservice-arch.yaml │ ├── sequence.yaml │ └── state-machine.yaml ├── diagrams/ │ ├── order-service/ │ │ ├── arch.d2 │ │ └── flow.d2 │ └── user-service/ │ └── arch.d2 └── output/ ├── svg/ └── png/这个结构的关键是分层清晰:tokens 是最底层,components 引用 tokens,templates 引用 components,diagrams 引用 templates。output 是渲染产物,不进版本库(或者只进最终发布版本)。
注意:diagrams 目录按业务模块分子目录,而不是按图类型分。因为实际工作中,人们找图是按“哪个服务的图”来找的,不是按“流程图还是架构图”来找的。
4.3 编写第一个图表描述文件
假设我们要画一个订单服务的架构图,用 D2 语法大概是这样:
vars: { d2-config: { theme-id: 0 layout-engine: dagre } } order-service: { label: "订单服务" shape: rectangle style: { fill: "#4A90D9" stroke: "#3A7BC8" font-color: "#FFFFFF" } } order-db: { label: "订单库" shape: cylinder style: { fill: "#F5F5F5" stroke: "#CCCCCC" } } order-queue: { label: "订单队列" shape: queue style: { fill: "#FFF3E0" stroke: "#FFB74D" } } order-service -> order-db: { label: "读写" style.stroke-dash: 0 } order-service -> order-queue: { label: "发布事件" style.stroke-dash: 3 }这个文件里,节点样式和连线样式都是内联写的。在实际项目里,这些应该抽成 component 和 token,通过变量引用。但第一次搭建时,先内联写能快速看到效果,确认布局和结构没问题之后,再逐步抽取。
4.4 渲染与自动化集成
描述文件写好后,用 CLI 渲染:
d2 --theme 0 --layout dagre diagrams/order-service/arch.d2 output/svg/order-service-arch.svg如果要批量渲染整个目录:
find diagrams -name "*.d2" | while read f; do out="output/svg/$(echo $f | sed 's|diagrams/||; s|\.d2|.svg|')" mkdir -p "$(dirname $out)" d2 "$f" "$out" done集成到 CI 里,可以在每次提交时自动渲染所有图表,把产物推到文档站点。这样文档里的图永远是最新的,不会出现“代码改了图没改”的情况。
我实际用下来,这套流程最大的收益是图表和代码同步更新。以前架构图是季度更新一次,现在每次合并请求都会触发渲染,图永远反映当前状态。这个改变对团队文档质量的影响是质变的。
5. 常见问题与排查技巧实录
5.1 布局错乱:节点重叠与边交叉
布局问题是最常见的。节点重叠通常是因为nodesep太小,或者节点尺寸差异太大导致算法计算失误。解决办法是先检查节点尺寸是否合理,如果某个节点特别大,考虑拆分或缩短文字。边交叉则更复杂,层次布局在有环图或者跨层连接多的时候容易交叉。
我的排查顺序是:先看rankdir是否合适,有时候把 TB 改成 LR 交叉就少很多;再看是否有不必要的跨层边,能不能通过增加中间节点来引导布局;最后才考虑调edgesep和ranksep。如果实在无法避免交叉,接受它,不要为了消除交叉把图改得面目全非。
5.2 字体渲染不一致
不同环境下渲染出来的字体可能不一样,尤其是中文字体。本地看着好好的,CI 里渲染出来变成方框或者默认字体。原因是渲染环境没有安装指定字体。
解决办法有两个:一是在渲染前确保环境安装了字体,比如在 CI 脚本里加apt-get install fonts-noto-cjk;二是把字体文件打包进项目,渲染时指定字体路径。我推荐第二种,因为环境依赖越少越稳定。
提示:中文字体文件通常很大,如果项目对产物体积敏感,可以考虑用字体子集化工具,只保留用到的字符。
5.3 主题切换后部分元素没变
前面提到过硬编码的问题。如果切换主题后发现某些节点颜色没变,八成是那个节点的样式写死了具体值,没有引用 token。排查方法是全局搜索颜色值,看有没有散落在描述文件里的硬编码。
预防措施是在 CI 里加一个检查,扫描描述文件里是否有#开头的颜色值,如果有就报警。这个检查很简单,但能省很多事。
5.4 渲染速度慢
图规模大了之后,渲染可能变慢。层次布局算法在节点数超过几百时会有明显延迟。如果遇到这种情况,可以考虑几个方向:一是拆分大图为多个小图,用链接关联;二是换用更快的布局引擎;三是缓存渲染结果,只有描述文件变化时才重新渲染。
我处理过一个有三百多个节点的依赖图,渲染一次要十几秒。后来拆成按模块分组的多个子图,每个子图几十个节点,渲染时间降到一秒以内,而且可读性反而更好了。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 节点重叠 | 间距参数太小 | 调大 nodesep/ranksep |
| 边交叉严重 | 布局方向不合适 | 尝试切换 rankdir |
| 字体显示异常 | 环境缺字体 | 安装字体或打包字体文件 |
| 主题切换不生效 | 样式硬编码 | 搜索颜色值,改用 token |
| 渲染超时 | 图规模过大 | 拆分子图或换布局引擎 |
| 导出图片模糊 | 分辨率不足 | 导出 SVG 或提高 DPI |
6. 进阶玩法:让图表真正“活”起来
6.1 从代码生成图表
diagram-design 最强大的地方在于,图表描述文件可以由程序生成。比如你可以写一个脚本,扫描代码里的服务依赖关系,自动生成架构图。这样图永远和代码一致,不需要人工维护。
实现思路是:解析代码里的注解或配置文件,提取服务名和依赖关系,输出成 D2 或 DOT 格式,再调用渲染器。这个脚本可以用任何语言写,Python 和 Node.js 都比较方便。
我做过一个类似的工具,从配置中心拉取服务注册信息,生成实时架构图。每次服务上下线,图自动更新。这个工具后来成了团队排查问题的第一入口,因为大家发现看图比看配置快多了。
6.2 交互式图表的可能性
静态 SVG 虽然好用,但有些场景需要交互,比如点击节点展开详情、悬停显示说明。这时候可以把渲染产物做成 HTML 页面,用 JavaScript 绑定交互事件。
D2 和 Graphviz 都支持输出 SVG,SVG 本身就可以绑定事件。你可以给每个节点加>