1. 从一张草图到一套系统:diagram-design 到底在解决什么问题
第一次看到 diagram-design 这个词,很多人会以为它只是“画图工具”的另一个说法。但真正在项目里被图表折磨过的人会明白,它指向的是一套更底层的东西:把“图”从一次性产物变成可维护、可复用、可协作的设计资产。我做过不少带架构图、流程图、状态机的项目,最深的体会是——画图不难,难的是三个月后你还能看懂自己画的那张图,难的是同事改了一个节点之后整张图没有崩掉,难的是同一套逻辑要同时输出给文档、幻灯片和代码注释时不用重画三遍。
diagram-design 要解决的核心痛点,就是图表的“设计一致性”和“结构可维护性”。它不是一个具体的软件,而是一种把图表当作设计系统来对待的思路:定义节点样式、连线规则、布局网格、语义色板,然后用一套可复用的规范去生成所有图。适合谁来参考?如果你是需要频繁输出架构图的后端工程师、要维护大量流程文档的产品经理、或者带团队做技术方案的设计者,这套思路能帮你省下大量重复劳动。哪怕你只是偶尔画一张流程图,理解它的分层逻辑也能让你少返工几次。
我见过太多团队的做法是:打开一个在线画图工具,拖几个框,连几条线,导出 PNG,贴进文档,完事。结果下一次需求变更,没人愿意去改那张图,因为改起来比重新画还累。diagram-design 的价值就在于,它把“改图”这件事的成本压到了最低——只要你的结构定义是清晰的,样式和布局可以自动跟随,改一个节点不需要手动挪动二十条线。
2. 核心思路拆解:为什么要把图表当设计系统来做
2.1 图表失控的三个根源
在讲具体做法之前,先搞清楚图表为什么会失控。我复盘过自己踩过的坑,也观察过不少团队的文档仓库,问题基本集中在三个地方。
第一个根源是样式硬编码。每个节点手动设置颜色、字号、边框,画的时候很自由,但一旦要统一调整,就得逐个改。更麻烦的是,不同人画的图风格不一致,放在同一份文档里像拼凑出来的。第二个根源是布局靠手拖。手动摆放节点位置,短期看很直观,但节点一多、连线一复杂,稍微动一个元素就牵一发而动全身。第三个根源是语义缺失。图里一个红色框到底代表“错误状态”还是“高优先级”还是“外部系统”,只有画的人知道,看图的人全靠猜。
diagram-design 的思路就是针对这三点:用样式令牌替代硬编码,用自动布局替代手拖,用语义标注替代纯视觉表达。这三件事听起来简单,但组合起来就是一套完整的方法论。
2.2 分层设计:结构层、样式层、渲染层
我习惯把 diagram-design 拆成三层来看,这样理解起来最清晰。
结构层描述的是“图里有什么”:有哪些节点、节点之间是什么关系、有没有分组、有没有方向。这一层用纯文本或结构化数据表达,比如用类似 DSL 的语法写A -> B: 请求,或者用 JSON 描述节点和边。结构层不关心颜色和位置,只关心逻辑。
样式层描述的是“图长什么样”:节点用什么形状、什么颜色、什么字号,连线是实线还是虚线,箭头样式如何。这一层用令牌(token)来定义,比如node-primary、edge-dashed、group-bg。样式层和结构层解耦,意味着同一份结构可以套不同的主题。
渲染层负责把前两层合成最终的图。它可以是代码渲染(比如用脚本生成 SVG),也可以是工具渲染(比如把结构化数据导入某个画图工具)。渲染层是可替换的,今天用这个工具,明天换那个工具,只要结构层和样式层还在,图就能重新生成。
这种分层的最大好处是关注点分离。改逻辑只动结构层,改风格只动样式层,换工具只动渲染层。我实测下来,一个中等复杂度的架构图,用这种方式维护的成本比手画低至少一半。
2.3 为什么不用“直接画”而用“定义后生成”
有人会问:我就画一张图,搞这么复杂干嘛?这个问题很合理。我的回答是:看图的生命周期。如果这张图只出现一次、永远不会改,那直接画确实最快。但现实中大部分图是要反复修改、多次复用的。一旦图的预期寿命超过一周,定义后生成的方式就开始体现优势。
举个具体场景:你维护一份系统设计文档,里面有五张图。某天系统加了一个新模块,你需要更新其中三张图。如果是手画的,你得打开工具、找到对应位置、挪动节点、重新连线、调整布局、导出、替换。如果是定义生成的,你只需要在结构层加一个节点和几条边,重新渲染,三张图一起更新。这个差距在长期维护中会被放大很多倍。
注意:定义后生成不是银弹。对于一次性的、高度定制化的视觉稿,直接画反而更高效。判断标准是这张图会不会被改第二次。
3. 核心细节解析:节点、连线、布局与语义的实操要点
3.1 节点设计:形状与语义的对应关系
节点是图的基本单元,它的设计直接决定了图的可读性。我在实践中总结了一套形状与语义的对应规则,用起来很顺手。
矩形通常表示普通处理单元或服务,圆角矩形表示有边界的组件或模块,菱形表示判断或分支,圆柱体表示数据存储,云形表示外部系统或第三方依赖。这套规则不是强制的,但一旦团队统一了,看图的人就能在几秒内建立预期。关键是不要混用——如果矩形有时表示服务有时表示数据,那形状就失去了语义价值。
节点内部的信息层次也很重要。我一般分三行:第一行是名称,第二行是类型或角色,第三行是可选的关键属性。字号上名称最大,类型次之,属性最小。这样即使图缩得很小,名称依然可读。颜色方面,我建议用语义色而不是装饰色:主流程用中性色,成功路径用绿色系,异常路径用红色系,外部依赖用灰色系。色板不要超过五种,否则图会显得杂乱。
3.2 连线规则:方向、样式与标签
连线比节点更容易被忽视,但它承载了图里最重要的信息——关系。我见过很多图,节点画得很精致,连线却一团乱麻,箭头方向不明、交叉严重、标签缺失。
方向方面,我坚持单向优先。如果两个节点之间是双向关系,宁可画两条平行线,也不要画一条双向箭头。双向箭头在复杂图里极容易产生歧义。连线样式上,实线表示同步调用或直接依赖,虚线表示异步消息或弱依赖,点线表示可选或已废弃的关系。这套约定让读者不用看说明就能大致判断关系性质。
连线标签是另一个关键点。标签要写“动词+名词”的形式,比如“发送请求”“返回结果”“写入日志”,而不是只写“请求”“结果”。动词让关系变得明确。标签位置尽量靠近连线中点,避免和节点重叠。如果连线太多导致标签拥挤,可以考虑把部分关系折叠到子图里。
3.3 布局策略:自动布局的取舍
布局是图表设计里最容易被低估的部分。手拖布局的问题前面说过了,但自动布局也不是万能的。我试过几种常见的自动布局算法,各有适用场景。
层次布局适合有明确方向的有向图,比如流程图、依赖图,它会把节点按层级排列,边尽量不交叉。力导向布局适合关系复杂的网络图,它会让节点自然散开,但结果不太稳定,每次渲染可能不一样。网格布局适合节点之间关系较弱的图,整齐但缺乏语义表达。
我的建议是:主图用层次布局,子图用网格布局,网络图才用力导向。层次布局的关键是控制好层级数量,超过五层就会变得很宽,这时候要考虑拆分成多个子图。另外,自动布局之后通常还需要微调,比如把关系紧密的节点拉近一点,把长边缩短。完全依赖自动布局有时候会得到“正确但难看”的结果。
3.4 语义标注:让图自己解释自己
语义标注是我认为 diagram-design 里最有价值但最少人做的部分。所谓语义标注,就是在图里嵌入可被机器读取的元信息。比如每个节点可以带一个type字段,每条边可以带一个kind字段。这些字段在渲染时可能只体现为颜色或形状,但在结构层它们是明确的数据。
这样做的好处是,图可以被程序处理。你可以写脚本检查“所有数据库节点是否都有备份路径”,可以统计“外部依赖有几个”,可以生成图的文字版摘要。我做过一个项目,用语义标注自动生成了架构图的变更日志——每次结构层改动,脚本对比新旧版本,输出“新增了两个服务、删除了一个外部依赖”。这在评审时非常有用。
提示:语义标注的字段命名要统一,建议用团队约定的小写英文加连字符,比如
>tokens: node: primary: fill: "#E8F0FE" stroke: "#1A73E8" text: "#1A1A1A" success: fill: "#E6F4EA" stroke: "#34A853" text: "#1A1A1A" danger: fill: "#FCE8E6" stroke: "#D93025" text: "#1A1A1A" external: fill: "#F1F3F4" stroke: "#5F6368" text: "#1A1A1A" edge: sync: stroke: "#5F6368" dash: "none" async: stroke: "#5F6368" dash: "6 4" optional: stroke: "#9AA0A6" dash: "2 4"这套令牌定义了四种节点样式和三种连线样式。颜色我选了低饱和度的填充配高对比度的描边,这样图在投影和打印时都清晰。字号和间距也建议放进令牌,比如
font-size: 14、node-padding: 12。定义好之后,所有图都从这里取样式,不再手写颜色值。4.2 第二步:用结构化语法描述图
结构层我推荐用文本 DSL,因为它比 JSON 更易读易写。下面是一个简化的例子,描述一个请求处理流程。
group "接入层" { node gateway [type: primary, label: "网关"] } group "服务层" { node auth [type: primary, label: "鉴权服务"] node order [type: primary, label: "订单服务"] node inventory [type: primary, label: "库存服务"] } group "存储层" { node order_db [type:>