立项做 diagram-design 这套方案,起因特别直接:我们团队每周都要产技术方案文档,画架构图、时序图、流程图的时间加一起,占比高得吓人。一开始大家用画图软件拖框连线,后来嫌效率低,试过不少文本即图表的方案,但总有不顺手的地方——要么图长太丑,要么布局调整起来还要写一堆声明,要么跟代码仓库的版本管理根本融不到一起去。后来我干脆自己做了一套以文本驱动为核心的图表设计工具,代号就叫 diagram-design。这篇文章就把这套东西的设计思路、核心实现、实操流程和我们踩过的坑完整盘一遍,干货偏多,适合要在项目中引入高效作图方案的技术负责人,也适合一个人扛下文档产出的全栈开发。
1. 内容整体设计与思路拆解
1.1 文本驱动到底在解决什么问题
diagram-design 的核心思路是把图表的一切——元素、位置、连线、样式——都写成领域特定语言的文本。这句话听起来简单,但背后解决的是传统画图工具的本质上没法解的痛点:图表跟代码一样需要版本管理、Review、复用和自动化生产,而拖动式的图形工具在这些维度上天然都是残缺的。
先说版本管理。拖拽式的图形文件存下来的基本是一大堆坐标和样式字段,两份文件落到一起,文本对比基本上没有任何可读性。就算你团队里用的是在线协作画图板,能看历史版本,但粒度通常只到“谁在什么时候改了一笔”,根本没有办法像代码提交那样精确到“新增了A服务到B服务的链路”这种语义级别。文本图表就完全是另一回事,代码提交的差异即图表的差异,Code Review 可以直接在文本层面做,使用者看到的改动是逻辑层面的改动,这对工程化协作极其重要。
再说复用。图形工具里的元素复制粘贴,进程内可以,换个文件基本就废了。文本图表可以写函数、可以套模板、可以定义公共组件,而且所有复用最终沉淀成文本之后,是用 Git 管理的,是全团队共享的。我们后面做的组件库本质上就是一个代码仓库,根本不是图形素材库,这一点决定了这套方案在上规模之后依然能保持可控。
最后是自动化生产。文本描述图表意味着图表可以由程序生成,架构读取代码仓库的拓扑结构,能自动画系统依赖图;接口文档的变更,能定时触发渲染出服务调用关系图。这类“图跟着代码长出来”的能力,拖拽式工具根本做不到。
1.2 工具选型的四个关键取舍
在设计 diagram-design 之前,我其实把市面上主流的文本作图方案都拿真实验证过一轮。老实说,如果某个现成工具完全满足需求,我不会去重复造轮子。做这个取舍的过程,本身也值得拿出来聊聊。
第一个关键取舍是要不要基于既有 DSL 还是自己设计。直接基于成熟的方案,好处是生态、社区解析器齐全;但问题也很明显——它们的语法设计面对复杂建模场景有局限,尤其是要表达“节点内部嵌套结构”和“面向侧边连线的自动布局”时,写出来的文本又绕又难读。自己设计 DSL 意味着解析器、渲染器、布局引擎全得自己扛,投入大,但换来的是语法完全贴合我们的场景,所有复杂图型都能用很直观的文本表达。权衡下来我选了后者,因为骨架固定下来之后,后续所有能力扩展都是往一个稳定的底座上加东西,长期收益更高。
第二个取舍是渲染目标。我先确定要渲染成 SVG,因为 SVG 是文本格式,可以嵌网页、可以转图片、可以批量处理,还能直接进代码仓库做可视化对比。另一点很重要,SVG 天生容易做主题切换,白天黑夜模式只需要换一套样式文件,不用重新生成数据。
第三个取舍是布局方案。自动布局省手但不可控,手动定位精度高但写法重。我的选择是折中:连线全部自动走路由,节点位置支持手动坐标和自动布局两种模式混用。复杂图里关键节点手动定位保证阅读逻辑,其余节点交给算法排,这样两边的优势都拿到。
第四个取舍是语法里要不要内置可控的排版能力。很多文本图表工具对“文本如何换行”“字体大小如何在局部调整”几乎没有控制力,导致生成的图虽然信息对但排版很丑。diagram-design 从一开始就把排版当一等公民,块级、行级、字级的样式能力全部做进 DSL 里,这条在后面做复杂大图的时候被证明是绝对正确的决定。
1.3 整体架构的几张关键切片
这套系统的架构并不复杂,从抽象层面分四层。第一层是语法层,负责定义 DSL 的词法与语法规则。第二层是解析层,把语法文本解析成中间表示,中间表示是所有后续处理的核心数据结构。第三层是布局层,负责把中间表示中的元素映射成具体坐标、尺寸、连线路径,既有确定性布局也有自动布局。第四层是渲染层,把布局结果转成 SVG 输出,同时承担主题映射、样式计算、支持导出。
四层各自独立,带来的直接好处是调试路径特别清晰。图表出问题,先看解析层有没有报错,再看布局层的坐标是否合理,最后才去查渲染层的样式计算。不用像调试图形工具那样瞎猜,省下来的时间非常可观。
2. 核心细节解析与实操要点
2.1 DSL 设计的核心场景与边界
diagram-design 的 DSL 第一版只有四类元素:节点、连线、泳道、分组。节点是最基础的表达单元,可以是方框、圆框、圆角框、棱形等基础形状,也可以进一步由子节点组合。连线表达点对点的关系,支持箭头类型、线型、标签、路径走向。泳道是横向或纵向的隔离带,常用于表达角色职责、流程阶段划分。分组是纯视觉的组织单元,用于视觉拢聚相关元素。
设计 DSL 语法时我给自己定的核心原则是“图即缩进”,什么意思呢?就是图表嵌套逻辑尽量用缩进表述出来,避免靠大量成对的显式语法符号来包夹,这样写起来顺手,读起来也直观。比如定义一个包含子节点的节点,直接在下方缩进写子节点,解析器通过缩进堆栈自动完成归属判断。
DSL 同时应该能做到局部表达不强求全图完整。在做复杂业务图时,经常是先在纸上划好主体结构,再用文本把主体搭出来,细节后面补。所以语法层面节点定义和连线定义是独立的,节点可以先只给名字不配样式,样式后续补充声明即可,这样增量式作图很顺畅。
目前语法覆盖的场景包括系统架构图、调用链路图、时序图、流程图、部署图,基本涵盖了日常文档八成的需求。剩下两成属于数据流图这种对建模能力要求更高的场景,语法也能表达,只是没有完全推出快捷语法糖,后续可以迭代加进去。
2.2 布局引擎的自动定位策略
自动布局这块我一开始用的是通用分层方案,适合 DAG 类图表的流向和层级排布。后来发现实际画图的时候,分层布局的结果常常出现长连线跨大片空白区域。看的人得在大脑里额外做一次视觉连接,很费劲,比手动定位差远了。
后来我把策略改成基于力的引导与分层方案混合。流程是先用分层得到主干方向,再用力的模型在局部做松弛化调整,把不必要的长线尽量折叠,让整体空间利用率、边交叉次数、节点对齐质量三个指标综合最优。实现之后明显感觉大图的可读性上了一个台阶。
实现布局引擎的时候,最容易被忽视的是“文本量对尺寸的影响”。节点内文本长度不同,节点宽窄差异会很大。布局算法必须让每一条连线的路由感知节点实际尺寸,不能让连线穿进节点内部。我们在布局前会先做一次文本尺寸预估,把所有节点的焊点坐标求出来,再交给路由算法。这步做早了后期渲染很舒服,做晚了一堆穿模。
2.3 主题系统与样式继承层次
主题能力是这类工具最容易做弱的地方。第一版实现里主题只有颜色配对的切换,结果就是换肤可以,但整张图的视觉层级、间距体系完全没办法调。后来我重构了样式系统,分层提前确定好:
- 全局主题基线,定义基础字体、基础颜色、基础间距。
- 元素类型默认样式,定义节点类型、连线类型、泳道类型的默认呈现。
- 实例级样式声明,针对具体某个节点或连线做局部覆盖。
- 运行时动态注入样式。
继承规则从全局到实例层层覆盖,实现在渲染阶段计算出最终样式,而不是在解析阶段就结算。好处是你改一个全局主题变量,所有图全部跟着变,但个别要突出的节点可以单独提亮提醒。
这套体系的实用价值在落地后特别明显。我们把各团队的架构图全部纳入统一主题体系之后,整套文档的观感直接拉齐了。以前每一份图都有自己的审美标准,有的花哨到读起来吃力的程度,现在配色范围被主题系统锁死,阅读负担小了很多。
2.4 可扩展机制和插件候选点
diagram-design 预留的扩展机制主要是节点渲染自定义和布局算法替换。我们内部做了一个组件库,核心围绕某些基础组件展开:基础节点支持分组嵌套;资源节点形似服务器或者数据库的“带底座”形状;决策节点做成六边形,带两侧锚点;时序相关节点带激活条。组件库定义在文本里只是一行声明,渲染层则对应一套代码实现,既能表达业务语义又能拖进任何一张图里去。
做插件化主要看主渲染流程中哪些点允许外部介入。我们定在四个口子供外部扩展:解析、布局、渲染、导出。解析阶段的插件可以做自定义语法糖;布局阶段的插件可以做行业专用布局,比如组织架构用的树状纵向布局;渲染阶段的插件可以做自定义形状的绘制;导出阶段的插件可以加滤镜,比如暗色模式、高斯模糊、手绘风。
3. 实操过程与核心环节实现
3.1 从零搭建一个最小可用的文本图表项目
这一节面向第一次接触文本生成图表的读者,从零讲怎么做。我的实际经验是别急着研究架构,先让“文本变出图”这条最短链路跑通,在此基础上再去加深。搭建顺序建议是这样的:先把 DSL 语法解析器和 SVG 渲染的最小版本跑起来,再补自动布局,最后做主题系统。
解析器我用的是经典的递归下降,一边扫描一边产出中间表示。那时代码量不大,几百行就能覆盖基础语法。渲染器最初只做了基础形状和连线的 SVG 输出,拿浏览器直接开 SVG 文件就能看效果。自动化布局和自定义主题都是后面才接进来的,因为第一版最重要的是能有效果,有正反馈。
真正决定项目能不能继往开来的,反而是个不起眼的点:命令行工具。我建议所有这类工具都必须给一个短平快的 CLI 入口,功能就是吃饭喝水两件事,读 DSL 文件并输出 SVG 文件、监听文件变化自动重渲。很多同事对折腾代码不感兴趣,但给一条命令就能让图表即时刷新,他们才会愿意用到日常工作中。
3.2 工程配置与关键算法参数
工程级落地时,需要关注几个配置项。输出画布尺寸还可以,但我更建议按内容自适应;只有极少场景才需要强制固定尺寸。字体建议做全局基线配置,字号建议按层级区分,比如大标题、块标题、正文、辅助说明,至少分出四级,这样图才谈得上排版。布局相关需要给三个可调参数:节点间距、分层间距、边的弯曲程度。
布局算法里有两个参数调优影响最大:一个是迭代次数,另一个是收敛阈值。设太小,布局结果都是乱的;设太大,渲染耗时翻倍,对超大图完全没必要。我的实际经验是迭代次数在能力范围内设小一点,渲染出来的图足够自然即可,不要盲目调高。老的经典方案经常为了最大程度压缩交叉数写过强的优化循环,结果一张 200 节点的图烘了十几秒,实际使用者根本等不起。
连线路由参数里有个信息我在参数选型时最警惕:连线拐角半径。默认折线连接质感硬,改成带圆角的折线之后,图的气质立刻大变。最初版本圆角半径统一是 4,后来全局调到 8,整张图肉眼可见地舒缓下来。这种细节虽然不写进需求文档,但直接影响观感和方案汇报的体感。
3.3 实操演示:用 diagram-design 画一张系统架构图
下面用一张典型的微服务系统架构图来走一遍实际流程。第一步是我在脑内把图的核心结构拆出来:客户端、网关、四个基础服务、两个数据存储、消息队列、监控告警链路。第二步是用 DSL 把结构落成文本,先定义节点,再定义连线,先不着急写样式。第三步是跑自动布局,看整体分布顺不顺眼;重点观察有没有刺眼的交叉线和过于紧密的邻接区域,有就手动调个别节点位置。第四步是套主题、调整局部样式的强调,比如热点链路改成高亮配色,下游依赖系统做成灰度风格,明确视觉层次。第五步是导出 SVG 放进文档里,并把 DSL 源文件提交到代码仓库。
实际遇到最多的问题集中在标签位置。自动布局把连线标签放在线段中点,但两条线挨得近时标签会重叠。我一般做法是支持手动指定偏移量,把标签往边上推一点;更智能的做法是根据连线角度自动算偏移方向,必要时微调。这块做到扰动少之后,图才敢拿出门。
3.4 渲染导出链路嵌入文档体系
文本图表最终还得落在文档流程里。目前我们的做法是在文档工程里加一个渲染任务,文档里的 DSL 代码块统一编译成 SVG 后引到正文中,加上哈希校验,源文件不变就不重复渲染。用的人只需要在文档中写代码块,提交后资源自动更新。图跟文天然同源,杜绝两张皮。
导出方面除 SVG 外还支持 PNG、PDF 两种最常用的格式。PNG 走浏览器无头渲染再截栅格图,密度按 2 倍和 3 倍两档;PDF 用于画布特别大的图,默认分页或缩放,能选。目前对完整长图的处理是导出时按渲染画布整体输出,尺寸非常大时先在源 DSL 里按区域切分,控制复杂度比较好。
4. 常见问题与排查技巧实录
4.1 解析阶段最常见的三类报错与处理
第一类报错是括号括号错配。DSL 不支持花括号包裹嵌套结构,而是用缩进表达,但有些元素定义仍然会用到中括号做修饰,在列表式的写法下很容易出现写一半少写一个右括号的情况。我的排查建议是格式化时打印语法树层级,能直接看出哪个节点层级不对,修起来快。
第二类报错是节点引用了未定义的连接点。连线的端点写错字,或者节点名引用错,在解析阶段并不会立即报错,因为解析器把连线当字符串存了。真正渲染时才在 SVG 里出现没有落点的线。后来在解析器里加了一层引用检查,所有连线端点必须在已声明节点集合里,才把这个坑填上。
第三类报错是泳道归属冲突。某些节点同时出现在两个泳道的缩进范围里,这种冲突解析器无法自动纠正。早期处理比较粗暴直接报错,后来改成以就近缩进优先,让用户能够快速看清错位。换言之,这种点多出现在复制粘贴后缩进错乱,用编辑器做一次格式整理基本能解决。
坑位总结一句话:缩进类问题能防御就防御,引用类问题能硬校验就硬校验。这两个原则守住之后,解析阶段的维护成本立刻从反复迷惑变成开源可查。
4.2 布局引擎效果不符合预期时的调参顺序
自动布局跑出来不好看,先别急着骂算法,按这个顺序排查:先看中间表示的结构对不对,节点之间的逻辑有没有建错;其次看有没有超大文本节点,它们是布局变形的大头;再看局部密度是不是过高,太高就先手动指定关键节点位置;最后一招才是调布局参数。
哪些参数值得调?首先试节点间距和分层间距,这是影响最大的两个;接着试边的弯曲系数,过大过小都会让人眼累;别轻易改迭代上限,除非你有明确的性能问题数据支撑。如果这些调完效果还是不理想,那大概率是图本身的结构不适合自动布局,手动定位反而是更优解。
自动布局从来不是万能的。我以前也迷信算法,总想把所有工作交给程序。后来发现某些业务图比如强调整体生命周期的时序类图,分层布局只会让人看不明白,手动按阅读顺序一列一列摆才是对的。工具应该给使用者选择权,而不是替你假设一切。
4.3 部分节点样式不生效的诡异问题
有一段时间遇到一个怪问题:全局主题该生效的地方全生效,但个别节点再怎么声明样式都不生效,而且不报错。查了半天发现原因是这些节点在首轮渲染后被缓存了,推理渲染更新时直接把缓存结果取出来了,完全没走样重算。缓存键当时只取了节点标识,没取样式版本,所以永远拿到的是老样式。
修掉之后我把缓存策略改成全局失效:任何一个主题变更都会清掉全量缓存而不是局部清理。代价是渲染速度掉一点,但解决了灵异问题。类似情况很可能在带深度嵌套的组里复现,建议如果遇到样式不更新先怀疑这层缓存。
4.4 常见问题速查表
| 现象 | 排查方向 | 解决建议 |
|---|---|---|
| 文本渲染成码 | 解析失败但没硬报错 | 检查缩进与括号闭合,看中间表示结构 |
| 连线悬空无终点 | 端点引用错名 | 开启解析期引用硬校验 |
| 自动布局线路交叉严重 | 主干节点无优先级 | 手动固定中间路径节点,让算法排枝叶 |
| 全局主题改了但局部不变 | 节点样式覆盖导致的层叠冲突 | 检查实例级样式覆盖声明 |
| 超大图渲染极慢 | 迭代次数过高 | 调低迭代上限,优先保证可达性 |
| 单独调一个节点位置后连线错乱 | 该节点未重新解析邻接关系 | 手动定位后强制刷新邻接路由 |
| 导出 PNG 模糊 | 像素比设置太低 | 按 2 倍、3 倍像素比重新导出 |
| 文档里的图片刷新不更新 | 资源哈希未失效 | 清理文档渲染缓存,重新提交源文件 |
5. 项目延展、工程化沉淀与个人经验补充
5.1 从单机工具到团队协作平台的演进
diagram-design 最早就是我自己本地写写用,后来一个组的人开始围观,接着干脆接进了文档工程。演进到这个阶段之后,真正值钱的就已经不是渲染引擎本身,而是围绕它沉淀下来的一整套协作规范:哪些图一定要走文本 DSL,哪些图可以直接用外部素材导入,统一主题清单谁来维护,新组件进组件库要走什么评审。
这根线拉长后,图表这件事就从“个人表达”变成了“团队资产”。各条业务线画出来的架构图、链路图、时序图,可以在同一条规范下互相拼装,拿来直接做跨团队方案合并,不再是各画各的、完全连不起来的图。
5.2 给后来者的一些体感建议
如果团队里也想搞类似的方案,我的体感是别追求一步到位。第一,先锁定 80% 最高频的作图场景做深做透,不要去覆盖所有图形类型;第二,把 CLI 体验放在绝对优先级,命令跑通了全组才会真用起来;第三,样式系统和主题能力要在项目早期就定型。主题这玩意晚了再改很痛苦,全图所有样式表都会跟着牵连,早改早点稳定。
还有一条容易被忽略但非常真实:要想清楚这套工具在未来维护周期里由谁来维护。独立工具最怕做的人走了之后没人接手,文档、测试、示例图全要同步沉淀下来。我们后期花了大量时间补回归用例,有了这套用例之后,每改一版渲染器都能快速发现有没有把已经调好的图打回原形,这一点救了很多次大版本升级。
5.3 最后分享两个实战技巧
结尾前再放两个小技巧。一是 DSL 里定义模板比想象中更好用,把常见的部署结构抽成模板之后,新业务接入时整个团队从画图两小时下降到改参数十分钟,模板直接带动生产成本下降。二是做 SVG 导出时给根画布加一层基础的统一标识,比如生成时间和源文件哈希,图表出问题时能快速定位是哪一版源文件、哪一次的渲染结果。
踩过不少坑之后,我最深的体会就是这类工具的成败往往不在引擎本身,而在于它能不能平滑地嵌入到团队既有的工作流里。剃刀锋利的前提是刀柄顺手,diagram-design 让我把这个道理实打实验证了一遍。如果你也每天在和图表缠斗,不妨从一套最小可用的文本图表方案做起,先让自己从拖拽的重复劳动里解脱出来,再一步步完善它。