尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

diagram-design:用文本声明把架构图和时序图变成可版本管理的代码

diagram-design:用文本声明把架构图和时序图变成可版本管理的代码 如果你和我一样维护过一个超过一年的技术文档仓库大概率会被同一个问题反复折磨代码版本已经迭代了三次可架构图还停留在最初那个粗糙的 draw.io 版本。我试过在 Figma 里把图画得漂漂亮亮试过在 Notion 里直接画白板也试过用 PlantUML 写文本生成时序图。最后让我真正稳定下来的是一个叫 diagram-design 的小项目——它用一套简单的文本声明语法把图表、流程图、时序图甚至简单动画都变成可版本管理的代码。这篇文章完全不打算写枯燥的宣传稿而是把我折腾文本画图工具这段时间里对设计思路、语法边界、踩坑记录和真实工作流的思考完整讲一遍。diagram-design 解决的核心问题只有一个让图表像代码一样可以被审查、被复用、被自动生成。它不是要取代 Photoshop 或者 Figma而是在技术文档配图这个具体到不能再具体的场景里提供一条比拖拽更可靠的路径。无论你是想给项目画一张架构图还是想在海量的接口调用里抽出一张时序图下面这些内容应该都能帮上忙。1. 为什么我把画图从设计软件里搬进代码仓库1.1 拖拽画图的三个隐性成本每一个都让人头疼最早我画技术图基本靠 draw.io后来换成 Figma。坦白说单张图的表达能力没有问题但一旦进入长期维护的状态问题就藏不住了。第一个成本是版本管理。draw.io 的文件虽然也是 XML但里面塞满了坐标、连线、样式 ID两个版本之间的 diff 基本不可读。哪怕只是把一个节点的字号从 12 改成 14diff 里可能就会出现上百行变化。你很难告诉 reviewer 我只是改了一个标题因为从文本上看这像是重画了一张图。Figma 更尴尬它的源文件在云端本地最多拿导出图想要像代码一样做 Code Review 是完全不现实的。第二个成本是协作割裂。代码评审已经习惯了 push / pull request / diff / comment 这一套但图不一样。设计图在某个设计软件里开发想改一个节点先得找到源文件再找到对应图层改完还得重新导出、上传。这套流程里每一步都可能成为阻塞点。最离谱的一次团队里没人装了对应的设计软件就为了改一个错别字我们专门找了一个有授权的同事远程操作。第三个成本是样式与内容的耦合。拖拽式工具里图的结构关系是靠坐标隐式表达的。想在最上面加一个节点意味着下面所有节点的 y 坐标都要跟着改想让一条线绕过某个图形意味着调整一堆贝塞尔曲线锚点。这种手工微调带来的工作量在一次性画图时还能接受在每两周一迭代的文档里就是无底洞。1.2 diagram-design 的定位把图形声明成文本diagram-design 的思路很简单每个图形元素都是一个文本声明。比如下面这段描述就足够生成一个矩形、一段文字和一条连线rect(auth-box, 80, 120, 220, 80, fill#E8EDFF, radius8) text(Auth Service, 120, 150, font_size16, weightbold) line(300, 160, 420, 160, arrowtrue)没有坐标面板没有鼠标拖动没有图层窗口。图就是一堆这样的声明存成.dgm文件跟着代码仓库一起走。改图的时候你改的是一行文本提交的时候你提交的是一个可以被 review 的文本 diffCI 跑的时候可以顺手把文本渲染成 SVG、PNG 甚至 HTML自动部署到文档站点。我特意把渲染后端定成 SVG而不是直接生成 PNG原因是 SVG 有几个对技术文档特别友好的特性。首先它是矢量格式放大到多少倍都清晰别人截图也不会糊其次它本身就是文本可以直接内嵌进 HTML 文档在浏览器里查看时还能做交互最后 SVG 的样式修改成本低换一套主题色甚至只需要改几行配置这一点在 1.3 节里会详细说。1.3 这东西适合谁不适合谁我也得泼盆冷水。文本画图并不是包治百病它有非常明显的适用边界。我根据自己的实际经验把它适用和不适用的场景列成了一个表适合场景不适合场景技术架构图、模块关系图需要精细排版的市场物料/海报时序图、状态机、流程图复杂的插画、人物、场景绘制需要频繁更新内容的文档配图需要像素级自由拖拽的脑暴草图需要纳入 CI/CD 自动生成的图追求手绘风格、随意感的视觉表达多人协作、代码审查的团队文档面向非技术用户的可视化白板判断标准很简单如果这张图的核心价值是信息关系那么文本声明就是最好的载体如果核心价值是视觉表现力那还是老老实实打开设计软件。我见过有人非要用文本工具画插画结果把自己折腾得够呛这属于用错了地方。2. diagram-design 的语法是怎么工作的2.1 坐标、元素和属性最小可用集很多文本绘图工具的问题在于语法太复杂光记关键字就劝退。diagram-design 在设计时坚持了一个原则元素 类型 名字 位置 通用属性。以最常见的rect为例rect(name, x, y, w, h, keyvalue...)name是这个元素的唯一标识后续连线、引用、分组都会用到它。x, y是左上角坐标默认原点在画布左上角x 向右递增y 向下递增单位是逻辑像素。w, h是宽高。后面可以接任意数量的样式属性比如fill填充色、stroke描边色、radius圆角、dash虚线。这种统一的形式有相当大的好处。你不需要为每个元素单独记一套 APIrect、circle、text、line的调用方式惊人一致circle(c1, 200, 200, r30, fill#F59E0B) text(token, 200, 200, anchorcenter, font_size14, fill#111827) line(l1, 200, 230, 200, 320, stroke#6B7280, dashedtrue, arrowtrue)text元素和其他图形最大的区别在于文字对齐。我见过很多人在文本画图时踩坑文字的 (x, y) 到底是左上角还是中心点diagram-design 里用anchor属性来控制可选值有left、center、right默认是left。画卡片标题时用left画节点中心文字时用center这样能省掉大量看起来差两个像素的微调。2.2 用变量和分组解决改一处动全身绝对定位坐标有一个老问题我把第一列所有节点整体向右挪 20 像素难道要改十处坐标diagram-design 用两个机制来解决一个是变量一个是分组。变量很好理解类似代码里的常量def svcName order-service def primaryColor #4F46E5 text(svcName, 100, 100, fillprimaryColor, weightbold)当你发现同一个服务名在五张图里出现了把它定义为变量是第一步。但这还不够因为变量只能管值管不了位置关系。分组更像是设计软件里的组概念group(gateway-group, x100, y80) { rect(gateway-bg, 0, 0, 220, 120, fill#FFFFFF) text(API Gateway, 40, 30, font_size18) line(internal, 20, 90, 200, 90) }注意区分组内部子元素的坐标是相对坐标也就是相对于组左上角(100, 80)的位置。这样一来移动整个组只需要改group行里的x, y组内所有元素跟着一起动。这比手动计算每个子元素的绝对坐标要靠谱得多。不过分组还有一个隐藏的坑分组内的元素名在全局引用时必须带前缀否则当多张图拼接时容易出现名字冲突。我的做法是约定组名作为子元素名的命名空间比如gateway-group.gateway-bg在代码里体现为rect(gateway-group.gateway-bg, 0, 0, 220, 120)这样既保证了唯一性也能在报错时快速定位到底是哪个分组里的元素出了问题。2.3 row 和 column让布局自动排开手动标坐标始终不是长久之计。当图中节点数量超过十个我更推荐把常见布局抽象成自动排列规则。diagram-design 内置了row和column指令专门解决N 个元素水平/垂直均布排列这个高频需求。举个例子如果我想画一条服务调用链API 网关、鉴权服务、订单服务、支付服务四个节点水平排列不需要手动计算每个节点的 x 坐标services row([ node(API 网关, w140, h64), node(鉴权服务, w140, h64), node(订单服务, w140, h64), node(支付服务, w140, h64) ], gap32, y120)row返回的是一个包含每个节点锚点位置的数组比如services[0].right表示第一个节点的右侧中点services[1].left表示第二个节点的左侧中点。这样后续画连线就变得极其舒适line(services[0].right, services[1].left, arrowtrue, label调用)这种设计背后的逻辑是人脑适合描述关系不适合计算坐标。你只需要告诉工具这四个节点排一行间隔 32y 坐标在 120剩下的均匀分布交给程序去算。如果中间要插一个新节点重新计算坐标的也不是你而是工具本身。为了处理更复杂的树状结构row返回的锚点可以继续嵌套再配合分组基本能覆盖我工作中八成的架构图布局需求。3. 一张登录链路时序图从零到产出的完整过程3.1 先想清楚要表达什么抽象讨论语法没什么感觉我用一张真实的时序图把完整过程走一遍。这张图描述的是客户端通过 API 网关调用登录接口鉴权服务校验 token最后从用户服务取出用户信息的链路。用自然语言描述需求大概是这样的客户端调用 API 网关的POST /login。API 网关调用鉴权服务的validateToken()方法校验 token。鉴权服务向用户服务请求getUserInfo()。用户服务返回用户信息。鉴权服务确认通过API 网关向客户端返回200 OK。时序图的核心是四个参与者以及它们之间的五条消息。开始写.dgm文件之前我一般先在纸面上确定参与者的顺序和消息的先后这一步无论是用文本还是拖拽工具都省不掉。3.2 第一版代码先把参与者和生命周期线搭出来diagram-design 里内置了actor组件它其实是一个复合组件由顶部的矩形、中间的文本和竖直虚线三部分组成。用起来非常简单canvas(960, 420) actor(Client, x80, y64) actor(Gateway, x280, y64) actor(Auth, x500, y64) actor(UserSvc, x720, y64)这里每个actor都生成了从顶部延伸到底部的虚线。为什么时序图一定要这条虚线因为它定义了参与者的生命周期范围后边所有消息都在这条线上对齐。我第一次用这个工具时觉得actor不过是一个矩形 文本 虚线的组合没什么了不起。但实际开发时发现把它封装成高级组件而不是让使用者手动画三条形状价值非常大。使用者关心的不是怎么画虚线而是这里有一个参与者它叫 Client。3.3 消息怎么自动对齐接下来添加消息。如果手工画一条消息要处理三条线段水平线 箭头 标签还要保证 y 坐标和两条生命周期线的交点对齐。这会把人逼疯。diagram-design 里的message指令把这些全部接管了message(Client, Gateway, POST /login, y140) message(Gateway, Auth, validateToken(), y200) message(Auth, UserSvc, getUserInfo(), y260) message(UserSvc, Auth, userInfo, y300, dashedtrue) message(Gateway, Client, 200 OK, y340)每个message需要提供发送者、接收者、文本标签和 y 坐标。工具会自动计算发送者生命线的 x 坐标、接收者生命线的 x 坐标然后画一条从左边到右边的水平线中间标上文本箭头指向接收者。我要做的只是决定每条消息放在哪一层 y 坐标以及是否需要虚线。如果你觉得y140这种绝对坐标还是麻烦项目还支持yauto模式它会在已有元素的最大 y 坐标基础上自动累加一个默认步长。实测下来大多数时序图都只需要在前几条消息上显式指定 y后面的用auto就行。3.4 渲染和输出保存为login.dgm后命令行执行diagram-design render login.dgm -o login.svg这条命令生成 SVG 文件。如果需要高清位图可以额外指定 scalediagram-design render login.dgm -o login.png --scale 3--scale 3的意思是按 3 倍逻辑分辨率输出适合 Retina 屏展示或者打印。我知道很多人习惯直接把 SVG 塞进文档但有些在线文档平台不支持 SVG 直接预览所以 PNG 输出也是刚需。我把整个文档项目的所有图都放进了 Makefilediagrams: diagram-design render docs/**/*.dgm --out docs/assets --ext svg一行命令递归处理docs目录下所有.dgm文件输出到docs/assets。现在团队改文档的习惯是改文字的地方顺带改.dgm然后跑一条命令图片自动更新。谁也不会再拿着旧图当新图了。4. 我用 diagram-design 踩过的坑和补救办法4.1 中文全部变成豆腐块第一次渲染出中文乱码时我第一反应是字符编码出问题了。检查完文件编码发现是 UTF-8完全正常。继续排查才发现SVG 在浏览器里显示中文没问题但当我用内置栅格化器导出 PNG 时系统找不到可用的中文字体于是所有中文字符都变成了豆腐块。解决办法是在配置里显式指定中文字体。我一般这样写font(familyPingFang SC, Microsoft YaHei, Noto Sans CJK SC, size14)如果是在 Linux 的 CI 环境里需要确认系统是否安装了中文字体。我踩过一次没安装fonts-noto-cjk的坑后来干脆在 Dockerfile 里固定安装好避免每次构建环境不一致。这个坑提醒我文本画图真正写进 CI 后字体渲染环境也是基础设施的一部分。4.2 标了--scale 3PNG 却还是模糊这个坑特别隐蔽。SVG 本身是矢量放大多少倍都不会糊但如果你只把 SVG 文件后缀改成.png或者用低分辨率截图工具转换自然还是糊的。diagram-design 的--scale不是简单放大画布而是在渲染时就按目标分辨率计算所有像素。但如果渲染后端依赖的某些样式属性比如阴影、模糊滤镜在缩放时没有正确处理就可能导致边缘发虚。更实用的建议是如果你要生成 2 倍图直接把画布逻辑尺寸写成最终尺寸的一半然后用--scale 2输出。这样坐标系统不用变最终尺寸正好是你想要的。不要先放大画布再缩小那样你会被坐标问题搞到崩溃。4.3 画完的线被矩形盖住这是一个 z-index 问题。diagram-design 按照声明顺序绘制元素后声明的元素会覆盖先声明的元素。默认情况下如果我先画一条连线再画一个矩形矩形会把连线压住。解决思路有两个。第一个是调整声明顺序把背景矩形、容器框放在最前面连线放在中间文本和图标放在最上层。第二个是显式指定z属性line(l1, 0, 0, 200, 200, z10) rect(bg, 0, 0, 200, 200, z1)我的习惯是所有用户可见的图形都不依赖默认层级而是显式给一个z值。这样别人改动时不会因为插入一行代码导致层级关系突变。这个坑的根因还是文本描述天然是扁平的需要自己补上层级意识。4.4 节点太多手动放不下画一张包含三四十个节点的全局架构图时手动指定每个节点的坐标就太痛苦了。我一开始还硬着头皮一个个填结果填到一半发现某个区域空间不够全部要重算。后来我学乖了优先用 2.3 节的row和column做自动布局实在复杂的关系图我会先画一张主图讲主干流程再画几张子图讲每个模块的内部结构最后用链接把它们串联起来而不是硬塞在一张图里。这里也涉及到一个思路转变文本画图工具的优势是生成而不是模拟手工。如果某张图需要大量手工微调坐标那就说明布局抽象不对应该先重构布局指令。4.5 多人协作时的命名与格式规范项目维护到第二个月新问题来了团队里不止我一个人写.dgm文件每个人对元素的命名习惯不同有人用rect1有人用login-box还有人在属性顺序上随缘。一旦图变复杂这种混乱会直接污染 diff。我现在在团队里推行了三条规定效果不错所有元素必须有语义化名称禁止rect1、line2这类无意义名字。每个.dgm文件开头必须有canvas和font两个声明。通用颜色和字体必须引用全局主题禁止在单个文件里写死。看到这里你可能觉得这是小题大做。但我不这么认为——当文本画图的优势和代码一样是可审查时它就必须像代码一样遵守规范否则优势会迅速变成灾难。5. 真实使用下来文本画图最关键的一点最后分享一点个人体会。diagram-design 用下来我最深的感受是文本画图最大价值不是让你少拖动鼠标而是让审图这件事成为可能。以前画在 draw.io 里的图只有打开文件的人能看见。现在.dgm文件在 PR 里被 diff评审者能直接看到某个节点名改了、某条线从实线改成了虚线、某个组件又被包了一层 group。这种细粒度的变化追踪是拖拽工具给不了的。如果你也想在自己项目里尝试建议第一张图不要选太复杂的用一两个小时画一张简单的架构图跑通render流程再慢慢加布局、样式、分组。等你习惯了改文本重新生成看结果这个循环大概率就回不去手动调坐标的时代了。
返回列表