1. 从“diagram-design”这个标题里,我读出了什么
第一次看到“diagram-design”这个标题时,我脑子里蹦出来的不是某个具体工具,而是一类非常典型的工程需求:把抽象的系统结构、数据流向、状态变化,用一张图讲清楚。这件事听起来简单,做起来却处处是坑。我见过太多团队,代码写得漂漂亮亮,一到画架构图就变成“方框加箭头,连上就算赢”,最后图没人看、没人维护,三个月后连作者自己都说不清那条虚线到底代表什么。
所以这篇内容,我想聊的不是“怎么用某个画图软件”,而是diagram design 这件事本身的方法论——从需求拆解、图形语义定义、布局算法选择,到最终产出可维护、可版本化、可自动生成的图表。关键词就三个:结构表达、视觉语法、自动化生成。适合谁看?如果你写过技术文档、画过流程图、做过系统设计评审,或者正在考虑把图表纳入 CI/CD 流水线,那这篇内容应该能帮你省下不少返工时间。
我自己的背景偏后端和基础设施,画过的图从简单的部署拓扑到复杂的多状态机都有。踩过的坑包括但不限于:用错图形语义导致评审被怼、手绘 SVG 后期改到崩溃、Mermaid 渲染出来的布局惨不忍睹。下面这些经验,基本都是被现实毒打之后总结出来的。
2. 图形语义先于视觉风格:别让箭头变成装饰
2.1 为什么大多数人画图的第一步就错了
很多人拿到画图任务,第一反应是打开工具,拖几个方框,连上线,调调颜色,导出 PNG。这个流程的问题在于:你把“视觉表现”放在了“语义定义”前面。结果就是,图上的每个元素含义模糊——这个圆角矩形代表服务还是模块?这条实线是同步调用还是数据依赖?虚线又是什么?
我吃过这个亏。早期做系统架构图,我用实线表示“调用关系”,虚线表示“数据流向”,结果评审时有人问:“那这个虚线箭头为什么从数据库指向缓存?”我才意识到,数据流向和调用关系在某些场景下是反向的,一张图里混用两种语义,读者根本分不清。
正确的做法是:先定义一套图形语法,再动手画。这套语法至少包含三个维度:
| 维度 | 常见取值 | 说明 |
|---|---|---|
| 形状 | 矩形、圆角矩形、圆柱、菱形、平行四边形 | 分别对应模块、服务、存储、决策、数据 |
| 线型 | 实线、虚线、点线、粗线 | 分别对应强依赖、弱依赖、异步、关键路径 |
| 箭头 | 单向箭头、双向箭头、无箭头、空心箭头 | 分别对应调用、双向通信、关联、继承 |
这套语法不需要多复杂,但必须在团队内达成一致,并且写进文档模板里。我后来在一个项目里推行了“三形状三线型”规则:矩形=服务,圆柱=存储,菱形=网关;实线=同步调用,虚线=异步消息,点线=配置依赖。推行之后,评审效率明显提升,因为大家不再纠结“这个框是什么意思”,而是直接讨论“这个依赖是否合理”。
2.2 图形语义的常见误区与修正
有几个误区我反复见到,这里列出来供你对照:
- 误区一:用颜色区分语义。颜色是最不靠谱的语义载体,因为色盲用户、黑白打印、深色模式都会让颜色失效。颜色应该只用于强调,不用于定义。
- 误区二:箭头方向随意。箭头方向必须和“控制流”或“数据流”一致,不能一会儿表示调用方向,一会儿表示依赖方向。如果两种流都需要表达,就用两种线型。
- 误区三:层级混用。把部署单元(如容器)和逻辑单元(如服务)画在同一层级,导致图看起来像一锅粥。正确做法是分层:逻辑架构图只画服务,部署图只画容器和节点。
我现在的习惯是,画图之前先写一段“图形语义说明”,哪怕只有三行字。这段说明会直接放在图的角落或者图注里。别小看这三行字,它能帮你省掉至少一轮评审扯皮。
2.3 从“画图”到“建模”的思维转变
真正高效的 diagram design,不是“画”出来的,而是“建模”出来的。什么意思?你把图背后的实体和关系抽象成一个数据模型,然后用渲染器把它画出来。这个模型可以是简单的 JSON,也可以是更正式的 DSL。
举个例子,如果你要画一张微服务架构图,不要直接拖框,而是先定义:
{ "nodes": [ {"id": "gateway", "type": "service", "label": "API 网关"}, {"id": "user-svc", "type": "service", "label": "用户服务"}, {"id": "user-db", "type": "storage", "label": "用户库"} ], "edges": [ {"from": "gateway", "to": "user-svc", "type": "sync"}, {"from": "user-svc", "to": "user-db", "type": "sync"} ] }有了这个模型,你可以用不同的渲染器输出不同风格的图,也可以自动检查“是否存在孤岛节点”“是否有循环依赖”。这才是 diagram design 的工程化思路。
3. 布局算法:为什么你的图总是“看起来乱”
3.1 手动布局的极限在哪里
我承认,手动拖拽在某些场景下是必要的,比如你要做一张信息图或者海报。但对于技术图表,手动布局的维护成本高得离谱。你改一个节点位置,可能要把周围所有连线重新调整;你加一个服务,整个图可能要重排。
更致命的是,手动布局很难保证一致性。同一个系统,两个人画出来的图可能完全不同,因为每个人对“美观”的理解不一样。这在团队协作中是灾难。
所以我的建议是:技术图表尽量用自动布局。自动布局算法虽然不完美,但它至少保证了“规则一致”。你只需要调整算法参数,而不是手动挪每一个框。
3.2 常见布局算法及其适用场景
不同的图结构适合不同的布局算法。下面这张表是我在实际项目中总结的:
| 算法 | 适用图类型 | 优点 | 缺点 |
|---|---|---|---|
| 层次布局 | 有向无环图、流程图、依赖图 | 层次清晰,方向明确 | 遇到环会退化 |
| 力导向布局 | 网络拓扑、关系图 | 自然聚类,适合探索 | 结果不稳定,每次不同 |
| 正交布局 | 架构图、部署图 | 线条横平竖直,整洁 | 节点多时容易交叉 |
| 圆形布局 | 环形依赖、对等网络 | 对称美观 | 不适合层次结构 |
我个人的经验是:架构图优先用层次布局 + 正交连线,因为技术读者习惯从左到右或从上到下的阅读顺序。力导向布局更适合“探索性”场景,比如你还不清楚系统里有哪些模块,先用力导向跑一遍,看看聚类情况,再手动调整成层次布局。
3.3 布局参数的微调技巧
自动布局不是“一键生成”就完事了,参数微调很关键。以层次布局为例,有几个参数你一定要注意:
- 层间距:太小会显得拥挤,太大会浪费空间。我的经验值是节点高度的 1.5 到 2 倍。
- 节点间距:同一层内节点之间的水平距离,建议是节点宽度的 1.2 倍左右。
- 边路由:正交连线要设置“避免节点”和“最小拐弯数”,否则线会穿过节点,看起来像蜘蛛网。
- 方向:从左到右适合流程,从上到下适合层级。混合方向会让读者迷惑。
还有一个容易被忽略的点:边的交叉最小化。好的布局算法会自动减少交叉,但你可以通过调整节点顺序来进一步优化。比如,把关联紧密的节点放在相邻位置,交叉自然就少了。
4. 工具链选型:从手绘到代码生成
4.1 手绘工具、GUI 工具与代码化工具的分界线
市面上的画图工具大致分三类:
- 手绘类:适合快速草图、白板讨论。优点是自由,缺点是难以维护和版本化。
- GUI 类:适合一次性交付的图表,比如汇报 PPT。优点是上手快,缺点是修改成本高,难以自动化。
- 代码化类:适合需要频繁更新、纳入版本控制的图表。优点是可 diff、可复用、可自动生成,缺点是有学习成本。
我的选择很明确:技术图表一律代码化。原因很简单——技术图表是“活”的,系统变了图就要变。如果图是手绘的,改图就成了负担;如果图是代码生成的,改代码就行,还能顺便 review。
4.2 代码化图表工具的对比与选型
代码化图表工具这几年发展很快,我主要用过这几类:
| 工具类型 | 代表方案 | 适合场景 | 我的评价 |
|---|---|---|---|
| 文本 DSL | 类 Mermaid 语法 | 快速嵌入文档 | 上手快,但布局控制弱 |
| 图形 DSL | 类 Graphviz 语法 | 复杂依赖图 | 布局强,但语法晦涩 |
| 编程库 | 类 D3、类 GoJS | 高度定制 | 灵活,但开发成本高 |
| 模型驱动 | 自定义 JSON + 渲染器 | 企业级架构图 | 可控性强,需要前期投入 |
如果你只是想在文档里嵌几张流程图,文本 DSL 足够了。但如果你要做一套“架构图即代码”的体系,我建议走模型驱动的路线:定义自己的数据模型,写一个渲染器,把模型转成 SVG 或 PNG。这样你可以完全控制图形语义、布局规则和视觉风格。
4.3 把图表纳入版本控制与 CI 流水线
代码化图表最大的好处是可以纳入版本控制。我现在的做法是:
- 图表源文件(JSON 或 DSL)和代码放在同一个仓库。
- 每次提交代码时,CI 自动重新渲染图表。
- 如果图表有变化,CI 会在 PR 里附上新旧对比图。
- 评审时,代码和图表一起看,确保一致性。
这套流程听起来复杂,但实现起来并不难。核心就是一条命令:render-diagram --input model.json --output diagram.svg。你可以把它包装成 Makefile 或者 npm script,集成到现有的 CI 里。
注意:渲染出来的 SVG 建议直接提交到仓库,而不是每次构建时生成。这样可以让 diff 更直观,也避免渲染环境不一致导致的问题。
5. 视觉语法设计:让图“自解释”的几个原则
5.1 颜色、形状、线型的语义分配
前面说了图形语义要先于视觉风格,这里展开讲一下视觉语法的设计原则。我的经验是:形状承载“是什么”,线型承载“怎么连”,颜色承载“多重要”。
- 形状:矩形=服务/模块,圆柱=存储,菱形=决策/网关,平行四边形=数据/消息。形状种类不要超过五种,否则读者记不住。
- 线型:实线=同步,虚线=异步,点线=配置/元数据,粗线=关键路径。线型也不要超过四种。
- 颜色:只用于强调,比如红色表示“已废弃”,绿色表示“新增”,灰色表示“外部系统”。不要用颜色区分“服务类型”,因为颜色在黑白打印时会丢失。
还有一个细节:图例是必须的。哪怕你觉得“这么简单还需要图例?”,也要加。因为你的读者可能第一次看你的图,他们没有你脑子里的那套语义。
5.2 信息密度与可读性的平衡
一张图能承载多少信息?这个问题没有标准答案,但有一个经验法则:如果一张图需要超过 30 秒才能看懂主干,那就太密了。
我见过一些架构图,恨不得把每个类的每个方法都画上去,结果就是一团乱麻。正确的做法是分层表达:
- L0 图:只画系统边界和外部依赖,不超过 10 个节点。
- L1 图:画主要服务及其关系,不超过 20 个节点。
- L2 图:画单个服务的内部模块,不超过 15 个节点。
每张图只回答一个问题。L0 回答“系统边界在哪”,L1 回答“服务怎么交互”,L2 回答“服务内部怎么组织”。不要把三个问题塞进一张图。
5.3 图注、标签与命名的规范
标签命名是最容易被忽视的细节。我见过太多图,节点上写着“服务A”“模块B”,这种命名毫无信息量。好的命名应该自解释:
- 服务名:用业务能力命名,如“订单服务”“支付网关”,而不是“Service1”。
- 边标签:用动词或名词短语,如“创建订单”“查询用户”,而不是“调用”。
- 图标题:用“什么视角 + 什么范围”,如“订单系统 - 部署视图 - 生产环境”。
还有一个技巧:给关键节点加编号。比如“1. 网关”“2. 订单服务”,然后在图注里解释每个编号的职责。这样读者可以按图索骥,不用在图上找半天。
6. 从需求到成图:一个完整案例的拆解
6.1 需求澄清:这张图到底要回答什么问题
假设你接到一个任务:画一张“订单系统”的架构图。别急着动手,先问清楚:
- 这张图给谁看?开发、运维、还是产品?
- 要回答什么问题?服务划分、部署拓扑、还是数据流向?
- 有没有现成的模型或文档可以参考?
- 图的更新频率如何?是一次性交付还是持续维护?
这些问题决定了图的抽象层级和表达方式。如果给运维看,重点画部署节点和网络分区;如果给开发看,重点画服务依赖和数据流。
6.2 模型构建:用 JSON 描述节点与关系
假设我们确定要画一张“逻辑架构图”,给开发看,回答“服务怎么交互”。那么模型可以这样构建:
{ "title": "订单系统 - 逻辑架构", "nodes": [ {"id": "gateway", "type": "gateway", "label": "API 网关"}, {"id": "order", "type": "service", "label": "订单服务"}, {"id": "payment", "type": "service", "label": "支付服务"}, {"id": "inventory", "type": "service", "label": "库存服务"}, {"id": "order-db", "type": "storage", "label": "订单库"}, {"id": "mq", "type": "queue", "label": "消息队列"} ], "edges": [ {"from": "gateway", "to": "order", "type": "sync", "label": "创建订单"}, {"from": "order", "to": "payment", "type": "sync", "label": "扣款"}, {"from": "order", "to": "inventory", "type": "sync", "label": "锁库存"}, {"from": "order", "to": "order-db", "type": "sync", "label": "写入"}, {"from": "order", "to": "mq", "type": "async", "label": "发布事件"} ] }这个模型很简洁,但已经包含了所有关键信息。接下来就是渲染。
6.3 渲染与迭代:从草图到最终交付
渲染器可以根据模型自动布局。第一版出来之后,你可能会发现一些问题:
- 订单服务和支付服务之间的连线交叉了。
- 消息队列的位置不太合理,导致线绕了一圈。
- 图例没有显示,读者不知道虚线是什么意思。
这时候就需要迭代。你可以调整布局参数,或者手动微调节点顺序。但注意,所有调整都应该在模型层面完成,而不是在渲染结果上手动改。否则下次重新渲染时,你的手动调整就丢了。
我的一般流程是:模型 → 渲染 → 评审 → 调整模型 → 重新渲染。通常两到三轮就能定稿。
6.4 交付后的维护:让图跟得上代码变化
图交付之后,最大的挑战是维护。系统变了,图没变,图就变成了“历史文档”。我的做法是:
- 把模型文件放在代码仓库里,和代码一起 review。
- 在 CI 里加一个检查:如果代码里的服务列表变了,但模型没更新,就报警。
- 定期(比如每季度)review 一次图,确保和实际系统一致。
这套机制听起来有点重,但比起“图过期了没人知道”,这点投入是值得的。
7. 几个让我印象深刻的踩坑记录
7.1 一次因为图形语义不一致导致的评审翻车
早期我画过一张图,用实线表示“同步调用”,虚线表示“异步消息”。结果评审时,有人指出:“这条虚线从订单服务指向支付服务,但支付服务是同步的,为什么用虚线?”我才发现,我在画图时把“异步消息”和“弱依赖”混用了。虚线既表示异步,又表示弱依赖,语义冲突了。
修正方案:一种线型只承载一种语义。异步消息用虚线,弱依赖用点线。如果两种语义都需要表达,就用两种线型组合,比如“虚线+点线”。
7.2 自动布局出来的图“不能看”时怎么办
自动布局不是万能的。我遇到过一种情况:系统里有循环依赖,层次布局算法直接崩溃,画出来的图完全没法看。这时候有几个选择:
- 手动打破循环,把某个边标记为“反向”或“忽略”。
- 换用力导向布局,虽然不完美,但至少能看。
- 把循环依赖单独画一张图,用不同的布局算法。
我的经验是:循环依赖本身就是设计问题,画图时暴露出来是好事。与其强行画一张“好看”的图,不如把问题摆出来,推动架构优化。
7.3 图太大、节点太多时的分层策略
当系统有上百个服务时,一张图肯定画不下。这时候必须分层。我的分层策略是:
- 第一层:按业务域划分,每个域一个节点。
- 第二层:展开单个业务域,画内部服务。
- 第三层:展开单个服务,画内部模块。
每层用不同的图表示,通过超链接或编号关联。这样读者可以按需深入,而不是被一张巨图吓退。
8. 关于 diagram design 的一些个人体会
画了这么多年图,我最大的体会是:图的质量不取决于你用了什么工具,而取决于你想清楚了没有。如果脑子里是一团浆糊,再漂亮的工具也画不出清晰的图。反过来,如果你能把系统拆解成节点和关系,哪怕用最简单的工具,也能画出有价值的图。
另一个体会是:图是给人看的,不是给机器看的。所以一切设计都要以“读者能否快速理解”为最高优先级。形状、线型、颜色、布局,都是为这个目标服务的。不要为了“好看”而牺牲“好懂”。
最后分享一个小技巧:画完图之后,找一个没参与这个项目的人看一遍,让他复述图的内容。如果他复述得八九不离十,说明图是自解释的;如果他一脸茫然,说明你的图还有改进空间。这个技巧我用了很多次,每次都能发现一些自己没注意到的问题。