上周五晚上十一点业务方发来一条消息把所有模块名称从英文改成中文。我打开那张精心拖拽了一下午的架构图看着上百个框框和密密麻麻的连线陷入了二十秒的沉默。那个晚上我下定决心把维护了一段时间的 diagram-design 这个项目正式推到团队内部使用。它本质上不是又一个流程图画布工具而是一套用文本描述图、再自动渲染成标准图片的方案。图的任何改动都可以走 Git、参与评审、做 diff不再是一张只能“另存为”的图片。这篇内容主要给每天要产出架构图、流程图、时序图的开发者和技术文档作者想把画图这件事从“手工活”变成“写配置”。如果你也被画布拖拽、对齐、连线、改字体折腾过下面的内容应该能帮到你。1. diagram-design 到底在解决什么问题一次评审背后的效率账1.1 我从手动拖拽画图转向文本画图的三个理由手动拖拽画图这件事在只有三五张图、一两个人维护的时候其实没什么问题。真正让人崩溃的是图开始进入评审、进入流程、进入多人协作之后。第一个逃不掉的问题是版本控制。图片是二进制文件Git 只能告诉我“这张图变了”永远不会告诉我“哪个模块被挪走了、哪条线被删了”。我经历过好几次评审之后想回退到上一版发现根本不知道哪个文件是哪一版只能靠文件名加后缀比如最终版、最终版2、最终版_really_final最后自己也分不清楚。文本描述图就没有这个问题一次改动就是几行文本的变化提交信息里甚至可以写清楚“把订单服务拆成两个子服务”这种具体的变更说明。第二个问题是多人协作的冲突。在线白板或者共享画布虽然方便但当两个人同时拖拽同一个模块的时候要么互相覆盖要么需要有人一直在旁边协调。更常见的情况是A 同事在本地画了一版B 同事也画了一版最后合并的时候只能靠肉眼比对两张图片的差异。文本图天然适合解决这个问题你改你的文件我改我的文件冲突只在真正重叠的部分出现和写代码处理冲突的体验完全一致。第三个问题是信息密度。鼠标拖拽看起来很直观但表达相同的信息量花费的时间远高于写文字。一次架构评审通常要求画出服务依赖、调用关系、数据流向手动画图要把几十个节点铺开、拉线、调整重叠位置往往一两小时就过去了。而使用 diagram-design 之后同样的结构用文本描述只需要几十行十分钟内就能完成初稿后续调整更是只改几个字段的事。1.2 一张图的生命周期与文本形态的优势一张图从诞生到废弃其实和代码很像刚开始是草案接着进入评审评审之后要修改上线之后要随系统演进持续更新最后可能被新图替代。手动画图的生命周期管理几乎完全依赖个人自律图变了我得记得去更新对应图片。可人总会忘记于是“图片和现状不一致”就成了很多团队的常态。文本形态解决的是“让图跟上现实”的问题。因为图本身就是一份文本文件它可以和代码放在同一个仓库里代码变更触发图变更图变更又跟着代码一起提交。哪怕只是修改一个接口名字先去文本文件里找到对应的节点名称改完重新渲染一遍整个链路不超过几分钟。我在实际维护过程中发现这带来的最大改变不是省时间而是图开始被当作“活文档”而不是“墙上的海报”。文本形态还有一个很实际的好处可以写注释。很多图表达的是现状但读者真正想知道的是“为什么这么画”。手动画布很难在不破坏视觉的前提下解释设计原因而文本图可以在节点旁边写清楚“这个网关后续要拆掉这里先保留历史调用逻辑”。这些注释会随图一起进入版本管理比在 IM 群里发一段解释靠谱得多。2. 选型之前先搞清楚图数据模型节点、连线、组合与样式2.1 无论用什么方案都逃不掉这四个要素接触过几种文本画图方案之后我发现所有图表面上看千奇百怪但底层数据模型都出奇一致节点、连线、组合、样式。这四个要素是所有图表的通用原子想清楚它们再看任何工具都会轻松很多。我习惯用地图来做类比。节点就是城市地图上每一座城市就是一个点连线就是道路把城市连接起来组合是行政区划省、市、县把城市归类到不同层级样式则表达道路等级高速公路用红色粗线乡道用灰色细线。架构图也完全一样一个微服务是一个节点服务间的调用是一条连线业务域或者技术分层是组合而调用方式、协议、流量强度都可以通过样式表达。下面是我整理的对应关系表在建图之前最好先按这个思路过一遍数据要素地图类比架构图里的角色典型的可视化属性节点城市服务、模块、数据库、客户端颜色、形状、边框、图标连线道路调用、依赖、数据流、消息队列箭头方向、线型、颜色、标签组合行政区划业务域、技术层、环境分组矩形边界、底色、折叠区域样式道路等级重点模块、故障节点、灰度发布范围明度、饱和、粗细、闪烁只要把现有业务信息对应到这四个要素里图的骨架就自然出现了。我见过很多人画图混乱不是因为不会用工具而是压根没有先做“建模”这一步。2.2 声明式与命令式的取舍社区里的文本绘图方案大体分成两派。一派是声明式你只描述“有什么”比如有哪些节点、哪些连线、哪些分组至于节点摆在哪里、连线怎么绕开重叠交给渲染引擎自动布局。另一派是命令式或者脚本式你编写一段程序通过代码生成图数据再由绘图接口输出图片。我对这两种方式都有过实际尝试目前优先推荐声明式。原因很简单它足够慢、足够稳。声明式的元数据是一份静态配置不会因为运行环境变化导致同样的输入产生不同的输出。在团队协作里输入确定而输出一致的特性极其重要这意味着大家可以基于同样的配置讨论同一种结果。命令式适合数据量很大的场景比如从服务注册中心批量拉取几百个服务节点自动生成调用拓扑。这种场景手动维护配置文件不现实写段脚本动态生成图数据更合理。但它也有代价生成的脚本本身需要维护图变成程序的“副产品”出了问题要先查代码再查数据。我的建议是日常的架构图、时序图、流程图绝大多数场景用声明式配置就够用。等哪天真的遇到几百个节点需要批量生成时再考虑用脚本辅助产出配置文件。不要一开始就上脚本否则图的维护成本会从“改配置”变成“维护一套生成系统”。2.3 设计规范的四个颗粒度文本画图方案还有一个好处就是设计规范可以被“编码”进配置。手动画图时每个人都有自己的审美有人喜欢圆角矩形有人喜欢直角有人把颜色用得姹紫嫣红。文本图则可以通过统一规范让所有图保持一致的视觉风格。我沉淀下来的一套规范包含四个层面。第一是命名规范节点 ID 必须使用小写字母和下划线展示标签可以保持中文或大写避免同一个模块在文档和代码里叫两个名字。第二是颜色语义绿色表示稳定服务黄色表示评审中红色表示异常或待下线避免临时起意用颜色装饰。第三是线型语义实线表示同步调用虚线表示异步消息点划线表示数据访问链路。第四是层级规范组合层级不超过三层超过三层的图强制拆分防止一张图承载过多信息。这四个层面看起来简单实际上能解决很多隐性沟通成本。评审会上最常出现的对话是“为什么这个模块是蓝色”“因为画的时候心情好选了蓝色”有了颜色语义这种无关讨论自然就消失了。3. 一套可以直接开抄的 diagram-design 配置约定从架构图到时序图3.1 顶层结构图、节点、连线、分组这部分我会直接给出一个可复用的配置示例。以我维护的 diagram-design 项目为例它使用的核心配置文件是一份 JSON 结构整体分为图元信息、分组、节点、连线和样式五个部分。下面的示例是一个最简骨架{ title: 订单中心架构, groups: [ { id: gateway-layer, label: 流量接入层, nodes: [gw, lb] }, { id: service-layer, label: 业务服务层, nodes: [order-svc, stock-svc] } ], nodes: [ { id: gw, label: API网关 }, { id: lb, label: 负载均衡 }, { id: order-svc, label: 订单服务 }, { id: stock-svc, label: 库存服务 } ], edges: [ { from: gw, to: order-svc, label: HTTP }, { from: order-svc, to: stock-svc, label: RPC, style: dashed } ] }节点使用了独立声明的方式分组里的 nodes 字段只是引用节点的 ID不重新定义节点信息。这样设计的好处是节点可以在多个分组中出现或者在不属于任何分组的情况下独立存在。连线只需要填写 from 和 to加上一个可选 label 表示调用关系。样式字段放在连线里而不是节点里是因为大部分场景下需要重点强调的是“连接关系”而不是“单个节点”。3.2 架构图实战三层架构如何表达有了顶层结构画一张常规的三层架构图就很快了。我以“订单中心”为例通常需要表达客户端、接入层、服务层、数据库这四类角色。配置可以这样组织{ title: 订单中心部署视图, groups: [ { id: client, label: 客户端, nodes: [web, app] }, { id: access, label: 接入层, nodes: [slb, nginx] }, { id: biz, label: 服务层, nodes: [order, user, pay] }, { id: data, label: 数据层, nodes: [mysql, redis] } ], nodes: [ { id: web, label: Web端 }, { id: app, label: App端 }, { id: slb, label: 负载均衡 }, { id: nginx, label: 反向代理 }, { id: order, label: 订单服务 }, { id: user, label: 用户服务 }, { id: pay, label: 支付服务 }, { id: mysql, label: MySQL主库 }, { id: redis, label: Redis缓存 } ], edges: [ { from: web, to: slb, label: HTTPS }, { from: app, to: slb, label: HTTPS }, { from: slb, to: nginx, label: TCP }, { from: nginx, to: order, label: HTTP }, { from: nginx, to: user, label: HTTP }, { from: order, to: pay, label: RPC, style: dashed }, { from: order, to: mysql, label: SQL }, { from: order, to: redis, label: READ } ] }渲染出来之后整个图被四个分组框包围组内节点自动排布组间连线跨过分组边界。我在实际使用中几乎没有手工调整过节点坐标因为自动布局已经足够表达层级关系。如果某个分组内节点太多导致拥挤最简单的办法是给这个分组单独拆一张子图而不是强行调整坐标。3.3 流程与时序图实战把交互时序写清楚顺着这套配置思路继续扩展流程图和时序图也能用类似结构表达。我认为时序图是最值得优先文本化的图类因为手动画时序图时最怕的就是消息线来回交叉每次新增一个调用都要重排整条生命线。时序图的核心概念是生命线和消息。生命线是参与交互的实例消息则是在生命线之间传递的调用。下面是我使用的一种简化描述方式sequence 下单流程 { participant client as 客户端 participant order-svc as 订单服务 participant stock-svc as 库存服务 participant pay-svc as 支付服务 message client - order-svc : 创建订单 message order-svc - stock-svc : 预占库存 message stock-svc - order-svc : 库存结果 message order-svc - pay-svc : 发起支付 message pay-svc - order-svc : 支付回调 return order-svc - client : 下单成功 }这种文本结构表达时序关系非常直观消息自上而下排列天然符合时间顺序。它的阅读体验甚至比渲染后的图片还要清晰文本可以直接在评审文档里引用改动也只是插入一行消息描述。我在团队里推广时序图配置之后最常见的使用方式是先在文本里讨论交互流程确认无误后再渲染成图放到设计文档里。3.4 样式细节用语义代替临时审美样式这个维度最容易被人忽略又最能影响图的可用性。如果每次渲染出来的图都依赖工具默认样式那么“绿色稳定、黄色评审、红色异常”这类团队约定就无法落地。所以我在 diagram-design 里把样式做成了独立配置层允许为节点和连线指定一组语义标签。一个典型的做法是在节点配置里增加 style 字段取值为 stable、review、deprecated、danger 等枚举而不是直接写颜色值。渲染引擎根据枚举决定最终视觉呈现。这样做的价值在于当你想把“评审中”的颜色从黄色改成橙色时只需要改一处定义而不是去每张图里逐个替换颜色。另一个实用技巧是给连线增加方向提示。文本描述不会自动判断箭头方向如果你希望表达“依赖关系”建议统一使用 from 指向被依赖方这样在图上读起来完全符合人们的直觉箭头指向的是下游依赖。4. 实际渲染中的五个高频坑乱码、布局失控与分组失效的排查记录4.1 中文乱码从编码到字体的一整套排查链路文本画图最常遇到的问题就是中文乱码而且这个问题在不同环节表现不同。我第一次遇到时节点标签全部变成一排方框第一反应以为是配置文件编码错误。检查之后发现源文件本身就是 UTF-8问题其实发生在渲染环节渲染引擎依赖的默认字体不包含中文字形。完整的排查链路应该是这样的。第一步确认输入阶段用文本编辑器打开配置文件如果中文正常显示说明源文件没有问题。第二步确认渲染阶段先在本地开发环境渲染一张最小测试图只包含一个中文节点如果仍然乱码基本可以断定是字体问题。第三步进入字体修复在系统层面安装中文字体或者在渲染引擎配置里显式指定一个中文字体路径。第四步是导出阶段如果你生成的是 SVG注意字体是否被正确嵌入否则拿到别的机器上打开依然会乱码。我在实际维护中踩过一个更隐蔽的坑本地渲染正常但放到 CI 流水线里渲染就乱码原因就是 CI 镜像里没有安装中文字体。这类问题很难从本地复现最可靠的做法是在项目的依赖清单里显式加入字体安装步骤把“依赖系统字体”改成“显式声明字体资产”。4.2 布局失控为什么改了配置布局却没变声明式绘图的自动布局算法通常很强大但它不是万能的。我遇到过最让我困惑的情况是明明调整了节点顺序渲染结果却纹丝不动。后来仔细读文档才发现这类引擎在很多场景下并不是按照配置里的节点顺序排布而是按照图的连通性计算层级。比如你希望“客户端”在最左侧“数据库”在最右侧但自动布局发现数据库连回了一些前端节点就会重新排序形成环状或者交叉。解决布局问题的核心思路是理解引擎的“排布规则”。绝大多数自动布局底层都遵循一个简化原则有向边决定主方向单向依赖的节点会自动沿着边方向排列无方向边容易形成横向伸展。想让布局更符合预期优先检查连线方向是否统一而不是单纯调整节点顺序。如果确实需要强制控制位置我会使用分组和隐式层级把需要靠左的节点放进同一个分组给它们一个统一的排序权重字段。有些方案还支持“隐藏辅助边”通过一条不可见的线把节点固定在目标相对位置但这种技巧会消耗额外的心智负担能不用尽量不用。4.3 分组失效节点跑出边界和连线穿墙分组失效是另一个出现频率很高的问题表现有两种节点没有出现在期望的分组里或者连线直接穿过了分组边框。节点跑外界的常见原因是 ID 不匹配。看起来像是“我把节点放在了分组下面”实际检查发现分组中引用的是某个 ID而节点定义里用的是另一个 ID渲染引擎会忽略不存在的引用而不是报错。建议在配置解析阶段增加“引用完整性”检查出现未定义的节点引用时直接输出告警否则查找起来非常耗时。连线穿过分组框则更微妙。自动布局算法在绘制连线时默认用最短路径连接两个节点如果两个节点恰好位于分组的两侧相对位置连线就会径直穿过中间的分组。让连线绕开分组的办法通常是给连线增加“路由”属性或者把连线的起点和终点指定为分组边界上的锚点。如果渲染方案不支持锚点一个朴素但有效的办法是调整分组布局让同一方向的分组尽量相邻排布减少跨分组斜线出现的概率。4.4 版本兼容与团队协作隐忧文本画图的生态和代码一样也有版本兼容问题。一次渲染引擎的版本升级可能导致布局算法变化同一份配置文件输出的图片和之前完全不一样。这种变化发生在团队协作里是灾难性的昨天评审通过的截图今天重新渲染就面目全非但配置文件的 diff 却是空的。我的处理习惯是把渲染引擎版本固定下来通过锁文件记录在 CI 构建里使用同一个镜像。这样同一份配置在任何时间渲染都会得到同一张图避免“看不见的变更”。另一个团队协作要点是图片导出时机。纯文本配置虽然能进版本管理但很多文档系统最终需要的是 PNG 或者 SVG如果不把渲染产物一起提交文档站的图片可能停留在过期版本。所以我的做法是提交时同时更新配置文件和渲染产物并把产物的生成时间作为校验标识。5. 当成代码工程来维护让图参与评审、测试与文档发布5.1 给图写“单元测试”文本画图的最大潜力在于它可以像代码一样被测试。很多人听到“给图写单元测试”会觉得夸张但实际上只需要三步。第一步是语法校验检查配置是否符合既定的图描述规范包括 JSON 是否合法、字段类型是否正确、是否存在未知节点引用。第二步是结构校验检查是否存在孤立节点也就是没有被任何连线连接、也不属于任何分组的节点。孤立节点在大多数场景里意味着图中缺失了重要关系属于设计异常。第三步是业务校验用脚本扫描特定命名规则比如所有数据库节点必须被数据层分组包裹所有对外接口必须从接入层发起。这些规则用代码写死之后图的质量就从“靠人工审查”变成了“提交时自动检查”。5.2 接入文档站与自动发布把图接入文档系统之后我开始体会到这套方案的复利效应。每次合入代码CI 会自动重新渲染所有相关图表然后把图片上传到文档服务器。这意味着文档站上永远展示的是最新图而不是某个同事三个月前手工导出的“最新版”。具体实施路径并不复杂。在构建脚本里增加一个“渲染图片”的步骤扫描仓库中所有符合规则的配置文件调用渲染引擎输出标准化图片最后将图片与配置文件的更新时间戳一起打包发布。对于有复杂依赖关系的大型系统还可以按业务模块拆分配置目录每个目录包含自己的图配置和对应的渲染产物。当接口变更发生时开发者在改动代码的同时修改相应目录的图配置CI 再自动更新文档站。图的维护成本被压缩到几乎为零。5.3 关于“代码画图”的边界什么时候别用文本图我承认文本图不是万能的。它最适合表达结构化信息比如架构图、网络拓扑、时序图、流程图、类图。凡是能用“节点加连线加分组”抽象的问题文本图通常都能高效解决。但它不适合表达创意类视觉比如海报、插画、手绘风格的思维导图、数据可视化里的复杂图表。这类设计需要自由排布和强烈的视觉叙事用文本描述会事倍功半。另一个边界是探索期的草图。前期头脑风暴阶段图的形态快速变化用白板或者快速画布工具更顺手。我的习惯是用画布先把结构“想清楚”觉得收敛了再转成 diagram-design 的配置文件进入正式文档。从草图到文本的切换本身也是一次“建模校验”如果连怎么用节点和连线描述草图都想不清楚说明图本身的逻辑还不够完整。最后再分享一点个人经验。把图当成代码维护之后最受益的其实不是画图的人而是看图的人。每次评审会大家面对的不再是“某同事刚拖出来的样子”而是一份经过校验、版本明确、能追溯变更的活文档。如果在团队里从一张核心架构图开始试用两三个迭代之后你就会发现回不去了。
阅读完成 · 觉得有帮助?