"diagram-design"这个名字,乍看平平无奇,但如果你每天要画架构图、流程图、数据流图,就会明白一个能"用几行文本生成图表"的项目,到底能省下多少时间。这不是又一个拖拽式画板,而是一个把图表描述和渲染彻底解耦的小型系统——你用结构化的文本定义节点、连线、分组和样式,工具负责自动排版、渲染和导出。它可以做的事很多:写技术方案时顺手生成一张当前系统的服务依赖图,评审时直接对图提 comment(而不是截图贴到群里扯皮),甚至把它接进 CI,让每次构建都自动产出一份最新的拓扑图。适合谁来参考?给经常画图的人省时间,给想把图表沉淀成代码资产的人一个新思路,也适合正在做工具类项目的人看看一个轻量设计是怎么落地的。1. 项目到底想解决什么问题1.1 传统画图工具的痛点,你肯定也遇到过先说一个很常见的场景:某次架构评审前夜,你把架构图画好了,躺床上突然想起某个服务其实已经拆成了两个,于是第二天早上你得打开画板,手动拖动框线、重新连线、把文字一个个拽回到框里,可能还要因为箭头交叉而绕一大圈折线。这种"图比代码还难改"的体验,是很多人提到画图就头疼的根源。传统画图工具做的是"所见即所得",好处是上手快,坏处是它把图当成了一张一次性画布。图里每一个元素的位置、颜色、字体、连线的拐点,全都变成不可复制的"手工作坊资产"。一旦内容变化,你要么重画,要么花大量时间微调布局。更麻烦的是协作——你发给同事一张图,同事想修改,只能基于图片里的信息重新画一份,版本一多,根本分不清哪张是最新的。我在这个项目中体会最深的一点是:图的本质不是"画出来的样子",而是"元素之间的关系"。只要把关系描述清楚,长什么样、摆在哪、怎么配色,都是可以自动推导的。diagram-design 的出发点,就是把重心从"画"拉回"描述"。1.2 diagram-design 的核心设计取舍当时搭这个项目时,我最先定的原则是:绝对不做拖拽编辑器。不是拖拽不好,而是拖拽编辑器解决的是"临时画图"的问题,解决不了"图表要跟随文档长期演进"的问题。我的目标非常明确:让图表像代码一样,存放在版本仓库里,可以被 diff,可以被 review,可以让任何人在几分钟内通过文本理解图的结构。所以整个设计建立在三个核心取舍上:第一,声明式描述优先。你写的不是画布上每一个像素的位置,而是"系统 A 依赖系统 B,它们之间存在同步调用关系",排版的事情交给布局算法。这个思路和写 CSS 类似——你描述样式,浏览器负责渲染,不自己去算每个元素在哪个坐标。第二,自动布局为主,手动微调为辅。自动布局必然不是完美的,任何布局算法面对复杂图都会出现交叉和拥挤,所以系统里保留了手动锚点、固定层级、节点分组这样的"提示机制",让用户可以通过配置干预布局,而不是彻底放任算法。第三,以文本为单一事实来源。图表渲染的结果只是"产物",源文件永远是那份文本。这意味着你可以把一个图嵌进 Markdown 文档,可以放在仓库里做版本对比,可以写个小脚本批量生成几十张图——这是位图或者画板工程文件都做不到的。2. 核心功能拆解与选型思路2.1 灰度不同的图表类型与语法设计做一个图表系统,第一步是确定支持哪些图表类型。这个项目最初只支持两种:流程型和拓扑型。流程型适合描述步骤、状态流转、审批链路;拓扑型适合描述服务依赖、网络结构、模块关系。后来逐渐加了分组容器图(把一组节点圈在一个大框里表示子系统或泳道)、ER 图(实体和实体间的关系),基本上覆盖了日常技术写作 90% 的需求。语法设计是这里的灵魂。我参考了几个"图表即代码"的既有方案,发现它们的表达方式各不相同,但共同点是:都有"定义节点"和"定义连接"两步。我的语法设计尽量往直觉靠拢。节点可以用一个简单的标识符加上可选显示文本,连接用箭头符号表达方向:// 定义三个服务节点 用户端; 网关; 订单服务; // 定义依赖关系 用户端 -> 网关; 网关 -> 订单服务;如果要加备注、加样式,可以用大括号扩展属性:用户端 { 样式: 边框虚线; 说明: 外部访问入口; } 订单服务 { 颜色: #2d8cf0; }为什么这么设计?因为我希望语法具备"只看一遍就能背下来"的能力。图表文本如果写得太复杂,让人感觉像在写 XML,那用户宁可回去手动画图。简洁本身就是这类工具最重要的可用性指标。2.2 渲染引擎与导出链路图表定义好了之后,接下来的问题是:怎么变成一张漂亮的图?这一步我把它拆成两个子问题:布局计算和图形渲染。布局计算解决的是"每个节点画在什么位置"。流程型图表用分层布局:先按节点之间的依赖关系划分层级,保证箭头尽量朝同一方向,再在每一层内优化排列顺序,减少交叉。拓扑型图表则用力导向布局:给每个节点一个初始位置,然后模拟"相连的节点互相吸引、不相连的节点互相排斥"的物理过程,迭代若干轮后达到一个相对稳定的平衡状态。这两种布局算法是常见开源方案的标配,自己实现的核心部分是对它们的参数做了可调暴露,比如迭代次数、节点间距、斥力系数,让用户在遇到复杂图时能主动干预。图形渲染解决的是"怎么把位置画成像素或矢量"。我的实现里走的是 SVG 渲染。为什么不选 Canvas?核心原因在于 SVG 是矢量格式,缩放不糊,可以被搜索引擎索引、被屏幕阅读器读取,而且可以直接嵌入网页和 Markdown 文档。另一个原因是 SVG 的 DOM 结构天然适合做交互,如果以后要支持点击节点跳转、悬浮展示详情,成本比 Canvas 低很多。导出链路支持三种格式:原始 SVG、通过后端转换出的 PNG、以及一份纯文本的节点/边列表(用于机器读取和统计分析)。PNG 的生成我直接用了一个独立的图像转换工具,前端的系统只负责产出干净的 SVG,不掺合位图打包的逻辑。这样职责单一,SVG 本身也更容易做单元测试。2.3 主题与样式系统的思考样式系统是我当初差点略过、后来发现极其重要的模块。图表工具如果只有默认的黑白样式,用户生成的图会千篇一律;但如果像画板一样开放所有属性,又会丧失"图源于代码"的规范化优势。最终的方案是引入了"主题"这个概念。一套主题包含背景色、节点填充色、边框色、线条颜色、字体族、字号、间距系数。官方内置了三套主题:默认浅色、深色(适合夜间分享和投屏)、极简黑白(适合打印)。用户也可以自定义主题文件,通过 JSON 覆盖任意变量。这个设计背后的考虑是:让团队可以维护统一的视觉规范,比如公司主题色、标准字体,然后所有图自动遵守——这比每个人手工调出五种蓝色要省心得多。主题适用场景特点默认浅色文档插图、评审材料对比度均衡,适用性广深色主题投屏展示、晚间工作深色背景,重点高亮突出极简黑白正式文档、打印去掉一切装饰,只留结构自定义主题团队规范、项目定制变量驱动,一处修改全局生效3. 从零跑通一个最小实现3.1 项目结构与模块划分这个项目我按照"解析—构建—布局—渲染—导出"五个阶段来组织代码,每一阶段对应一个独立模块,模块之间通过数据结构衔接,不共享可变状态。目录结构大致是这样的:diagram-design/ ├── src/ │ ├── parser/ // 文本语法解析,产出元素树 │ ├── model/ // 元素树的数据结构与校验逻辑 │ ├── layout/ // 布局算法,层内排序、力导向模拟 │ ├── render/ // 从布局结果生成 SVG 字符串 │ ├── theme/ // 主题定义与变量合并 │ └── exporter/ // 导出 SVG/PNG/列表文件的入口 ├── tests/ // 解析、布局、渲染的单元测试 └── examples/ // 自带示例,方便快速上手为什么要按阶段拆?因为这样做每个环节都能独立测试。解析阶段出问题时,不需要去查渲染代码;布局效果不好时,可以把同一棵树喂给不同布局算法,对比结果。这种流水线式的架构对工具类项目特别友好,也方便后来者往里面加新的图表类型——你只需要保证输出符合 model 层的数据结构就行。3.2 核心流程:从文本到元素树再到布局整个流程可以用一条主线串起来。第一步是解析文本,这一步的本质是"把有结构的字符串变成有结构的内存对象"。我的解析器没有用正规文法或者复杂的词法分析工具,而是自己写了一个简单的逐行扫描器,识别节点定义行、连接行、分组块和属性块。因为语法本身就刻意设计得很简单,手写解析器反而更好维护,出错时的报错信息也更容易写得贴心。解析得到元素树之后,要做一次数据校验。比如检查是否有节点被定义了两次、连接指向的节点是否都不存在、分组是否有循环嵌套。这一步很有必要,因为后续布局算法对数据的合法性很敏感,一个悬空的引用就能让布局过程直接崩溃,提前校验能把问题暴露在更友好的阶段。布局阶段是最有意思的。以层级布局为例,第一步是拓扑排序,确定每个节点所处的层级序号。接着遍历所有跨层连接,给它们做"虚拟节点"处理——跨层连线被拆成多段短线段,这样后续的排序过程可以把连线交叉最小化当做一个排序问题来解决。最后根据层高和节点大小计算每个节点的具体坐标。这些算法本身是经典图论算法,但在工程落地时,大量的时间花在调参上:层间距设多大、节点之间的水平间隙是多少、如何处理孤立节点不参与分层。3.3 渲染成 SVG 的几个关键细节当每个节点都有了坐标之后,渲染阶段就相对直接了。我会遍历元素树,将每一个节点渲染成一个 SVG 图形(矩形、圆角矩形或圆形,取决于类型设置),将连接渲染成带箭头的路径。这里有一个非常值得注意的细节:箭头。很多人画图时觉得箭头"差不多对着就行",但用程序画箭头时,如果箭头的尖端直接对准目标节点的中心点(图形中心),箭头会被节点方块盖住,看起来就像陷进了矩形里。解决方案是先计算出连线与目标节点边框的交点,再让箭头终止于这个交点。这一步需要根据节点的形状类型(矩形、圆形、圆角矩形)分别做几何计算。听起来简单,实际坑很多,因为你画的不光是直线,还有曲线路径,曲线的终点方向要靠当前切线方向来确定。另外一个细节是 Clip 和遮挡关系。当节点尺寸比较小、文字比较长时,文字会溢出边界。我的处理是给节点内的文本加上最大宽度限制,超出部分截断用省略号表示,鼠标悬浮时可以查看完整内容(在支持交互的环境下)。这个策略虽然牺牲了一点展示完整性,但保证了大图中不至于出现大量文字重叠遮挡的"烂图"。渲染完成后,把 SVG 元素拼接成字符串,写入文件即可。SVG 的 header 部分要注意加上xmlns命名空间声明,否则某些工具打开会直接报错或不显示。4. 实操中躲不开的坑4.1 自动布局那点"薛定谔的乱"自动布局算法在简单图上是惊艳的,在复杂图上就是薛定谔的乱——你永远不知道它这次会不会给你一个满意的结果。我自己被这个问题困扰了很久。后来总结出的经验是:不要和布局算法硬刚,要给它"引导"。布局算法最早只接受节点和边,后来我给 model 层加了几个非强制属性,比如"节点建议方向"(left/right)、"层级优先级""分组锚定"。用户可以在文本里写出:网关 { 首选层级: 1; } 订单服务 { 首选层级: 2; }布局算法在计算时会把首选层级作为强约束加入拓扑排序,这样用户就能通过微调几个关键节点,把整张图的结构拉整齐。这个"提示机制"比给每个节点设置绝对坐标要优雅得多,因为它的意图明确——用户干预的是逻辑结构,而不是像素位置。提示:遇到布局乱时,先别改算法参数。先试着把关键的"骨架节点"加上层级或方向提示,大多数情况下比调参更有效。4.2 中文、特殊字符与字体的隐藏问题如果你画的是中文图表,字体这块大概率会踩坑。最早做出来的 SVG 在电脑上打开很正常,但某次在同事的电脑浏览器里打开,所有中文都变成了方块,排查了很久才发现是字体回退的问题。SVG 里如果没有显式指定字体,渲染引擎会依赖系统默认字体,但不同系统默认字体差异很大(Windows、macOS、Linux 各不一样)。最终方案是在主题配置里增加几组字体栈设置:{ "fontFamily": "'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', sans-serif" }这样在不同系统上,都能优先匹配到系统中文字体,实现相对一致的显示效果。另外,如果图表文本里包含类似<、&这样的字符,在生成 SVG 时如果不做转义,会让整个 XML 解析出错。所有用户输入的内容我都强制经过一道 HTML 实体转义,这既是安全要求,也是 SVG 合法性的基本要求。4.3 大图场景下的性能与内存问题画一张二十个节点的图毫无压力,但当你试图用程序批量生成几百个节点的依赖全景图时,性能问题就冒出来了。力导向布局算法的时间复杂度大约为平方量级,每次迭代都要计算所有节点对之间的斥力,几百个节点时画画还算流畅,上千个节点时迭代一轮就可能是好几秒。我的处理办法是分三层优化。第一层是减少迭代次数——不再等到完全收敛,而是设定一个最大迭代轮次,并且在每轮结束时计算节点移动的平均距离,如果低于阈值就提前终止。第二层是空间分区——把画布切成网格,只计算相邻网格内的节点斥力,大幅减少无效计算。第三层是降低 SVG 复杂度——对于密集区域,用一条路径合并多条连线,减少 DOM 节点数量。这里有个很关键的取舍:大图的渲染质量首先保证的是"能看懂结构",而不是"每一个节点都精致"。几十个节点的小图,宽松间距和饱满样式是优点;几百个节点的大图上,同样的样式会让整张图变成一坨色块。我后来引入了"密度感知"的样式策略:当节点数量超过阈值时,自动缩小节点面积、压缩间距、去掉圆角和阴影,只用最小视觉元素表达结构。4.4 常见问题速查表现象可能原因解决办法中文渲染成方块缺少中文字体配置在主题中设置字体回退栈,覆盖常见系统中文字体导出的 SVG 在某些工具里打不开缺少 XML 命名空间或实体未转义检查<svg>头部的xmlns属性,并转义特殊字符节点顺序错乱、箭头乱指没有进行拓扑排序就进入布局先做拓扑排序,再处理孤立节点连线盖住节点文字箭头端点计算未处理节点边界计算连线与节点形状的边界交点,以此为终止点大型图渲染卡顿布局计算量过大、SVG 节点过多开启空间分区、减少迭代、使用密度感知样式手动调整的部分总被布局覆盖布局算法优先权高于手动提示检查是否有强制布局参数,保留手动锚点的最高优先级5. 从项目到日常工作的延伸5.1 让图表变成可协作、可评审的沉淀物做这个项目之后,我最大的感触是:图表一旦变成代码资产,它的使用方式就完全变了。以前大家在群里发架构图图片,遇到改动只能发一个新图,还要嘴说"以最新这张为准"。现在某项目把系统架构图写成了 diagram-design 的声明文件,放在仓库的 docs 目录里。有人改了服务依赖关系,提交的 diff 里能直接看到哪一行从服务A -> 服务B变成了服务A -> 服务C。评审的时候对着文件改,直观且不容易漏。这种模式下,图表的生命周期和代码完全同步了。自动构建时顺手渲染一张最新的架构图上传到内网知识库,任何时刻看到的那张图,都对应着仓库的某一 commit。我后来在一个团队内部推广这个流程时,重点强调的其实是这一句话:别再把图当图,把它当成"可视化视图"源文件,图的维护成本就能压缩到几乎为零。5.2 后续还可以扩展的方向如果说这个项目现在还有哪些明显值得做的改进,我列了三个方向。第一个是交互式浏览模式。现在的渲染结果是一张静态 SVG,但 SVG 本身是支持事件绑定的。可以做一个简单的预览页面,支持点击节点高亮相关链路、拖动画布、放大缩小、查看节点关联的属性详情。这些功能不会影响核心架构,只需要在渲染层输出带数据属性的 SVG 节点即可。第二个是模板化生成。很多图的拓扑结构其实是有规律的,比如前端项目里的依赖图、微服务调用链。可以抽几个常用模板,用户只需要填关键内容,就能生成一整套图。比如"微服务架构模板":网关层、业务层、数据层各放几个节点,再填依赖关系。第三个是集成到文档工作流的插件。如果文档系统支持嵌入自定义节点,就能直接把 diagram-design 的源文件嵌进页面,实现"文档里看到的图就是本地源文件渲染出来的"。这样图就和文档彻底绑定,不再有截图同步问题。最后再分享一个我在做这个项目时意外学到的小技巧:别急着从零实现布局算法。很多图表系统的核心价值不在算法本身,而在于语法设计、样式系统和工程化集成。把布局算法换成现成的开源库,完全不影响项目的独特价值,反而能帮你把精力聚焦在真正有差异的地方。做工具类项目尤其容易陷入"什么都要自己写"的冲动,但克制住这个冲动,产品反而走得更远。
"diagram-design"这个名字,乍看平平无奇,但如果你每天要画架构图、流程图、数据流图,就会明白一个能"用几行文本生成图表"的项目,到底能省下多少时间。这不是又一个拖拽式画板,而是一个把图表描述和渲染彻底解耦的小型系统——你用结构化的文本定义节点、连线、分组和样式,工具负责自动排版、渲染和导出。它可以做的事很多:写技术方案时顺手生成一张当前系统的服务依赖图,评审时直接对图提 comment(而不是截图贴到群里扯皮),甚至把它接进 CI,让每次构建都自动产出一份最新的拓扑图。适合谁来参考?给经常画图的人省时间,给想把图表沉淀成代码资产的人一个新思路,也适合正在做工具类项目的人看看一个轻量设计是怎么落地的。1. 项目到底想解决什么问题1.1 传统画图工具的痛点,你肯定也遇到过先说一个很常见的场景:某次架构评审前夜,你把架构图画好了,躺床上突然想起某个服务其实已经拆成了两个,于是第二天早上你得打开画板,手动拖动框线、重新连线、把文字一个个拽回到框里,可能还要因为箭头交叉而绕一大圈折线。这种"图比代码还难改"的体验,是很多人提到画图就头疼的根源。传统画图工具做的是"所见即所得",好处是上手快,坏处是它把图当成了一张一次性画布。图里每一个元素的位置、颜色、字体、连线的拐点,全都变成不可复制的"手工作坊资产"。一旦内容变化,你要么重画,要么花大量时间微调布局。更麻烦的是协作——你发给同事一张图,同事想修改,只能基于图片里的信息重新画一份,版本一多,根本分不清哪张是最新的。我在这个项目中体会最深的一点是:图的本质不是"画出来的样子",而是"元素之间的关系"。只要把关系描述清楚,长什么样、摆在哪、怎么配色,都是可以自动推导的。diagram-design 的出发点,就是把重心从"画"拉回"描述"。1.2 diagram-design 的核心设计取舍当时搭这个项目时,我最先定的原则是:绝对不做拖拽编辑器。不是拖拽不好,而是拖拽编辑器解决的是"临时画图"的问题,解决不了"图表要跟随文档长期演进"的问题。我的目标非常明确:让图表像代码一样,存放在版本仓库里,可以被 diff,可以被 review,可以让任何人在几分钟内通过文本理解图的结构。所以整个设计建立在三个核心取舍上:第一,声明式描述优先。你写的不是画布上每一个像素的位置,而是"系统 A 依赖系统 B,它们之间存在同步调用关系",排版的事情交给布局算法。这个思路和写 CSS 类似——你描述样式,浏览器负责渲染,不自己去算每个元素在哪个坐标。第二,自动布局为主,手动微调为辅。自动布局必然不是完美的,任何布局算法面对复杂图都会出现交叉和拥挤,所以系统里保留了手动锚点、固定层级、节点分组这样的"提示机制",让用户可以通过配置干预布局,而不是彻底放任算法。第三,以文本为单一事实来源。图表渲染的结果只是"产物",源文件永远是那份文本。这意味着你可以把一个图嵌进 Markdown 文档,可以放在仓库里做版本对比,可以写个小脚本批量生成几十张图——这是位图或者画板工程文件都做不到的。2. 核心功能拆解与选型思路2.1 灰度不同的图表类型与语法设计做一个图表系统,第一步是确定支持哪些图表类型。这个项目最初只支持两种:流程型和拓扑型。流程型适合描述步骤、状态流转、审批链路;拓扑型适合描述服务依赖、网络结构、模块关系。后来逐渐加了分组容器图(把一组节点圈在一个大框里表示子系统或泳道)、ER 图(实体和实体间的关系),基本上覆盖了日常技术写作 90% 的需求。语法设计是这里的灵魂。我参考了几个"图表即代码"的既有方案,发现它们的表达方式各不相同,但共同点是:都有"定义节点"和"定义连接"两步。我的语法设计尽量往直觉靠拢。节点可以用一个简单的标识符加上可选显示文本,连接用箭头符号表达方向:// 定义三个服务节点 用户端; 网关; 订单服务; // 定义依赖关系 用户端 -> 网关; 网关 -> 订单服务;如果要加备注、加样式,可以用大括号扩展属性:用户端 { 样式: 边框虚线; 说明: 外部访问入口; } 订单服务 { 颜色: #2d8cf0; }为什么这么设计?因为我希望语法具备"只看一遍就能背下来"的能力。图表文本如果写得太复杂,让人感觉像在写 XML,那用户宁可回去手动画图。简洁本身就是这类工具最重要的可用性指标。2.2 渲染引擎与导出链路图表定义好了之后,接下来的问题是:怎么变成一张漂亮的图?这一步我把它拆成两个子问题:布局计算和图形渲染。布局计算解决的是"每个节点画在什么位置"。流程型图表用分层布局:先按节点之间的依赖关系划分层级,保证箭头尽量朝同一方向,再在每一层内优化排列顺序,减少交叉。拓扑型图表则用力导向布局:给每个节点一个初始位置,然后模拟"相连的节点互相吸引、不相连的节点互相排斥"的物理过程,迭代若干轮后达到一个相对稳定的平衡状态。这两种布局算法是常见开源方案的标配,自己实现的核心部分是对它们的参数做了可调暴露,比如迭代次数、节点间距、斥力系数,让用户在遇到复杂图时能主动干预。图形渲染解决的是"怎么把位置画成像素或矢量"。我的实现里走的是 SVG 渲染。为什么不选 Canvas?核心原因在于 SVG 是矢量格式,缩放不糊,可以被搜索引擎索引、被屏幕阅读器读取,而且可以直接嵌入网页和 Markdown 文档。另一个原因是 SVG 的 DOM 结构天然适合做交互,如果以后要支持点击节点跳转、悬浮展示详情,成本比 Canvas 低很多。导出链路支持三种格式:原始 SVG、通过后端转换出的 PNG、以及一份纯文本的节点/边列表(用于机器读取和统计分析)。PNG 的生成我直接用了一个独立的图像转换工具,前端的系统只负责产出干净的 SVG,不掺合位图打包的逻辑。这样职责单一,SVG 本身也更容易做单元测试。2.3 主题与样式系统的思考样式系统是我当初差点略过、后来发现极其重要的模块。图表工具如果只有默认的黑白样式,用户生成的图会千篇一律;但如果像画板一样开放所有属性,又会丧失"图源于代码"的规范化优势。最终的方案是引入了"主题"这个概念。一套主题包含背景色、节点填充色、边框色、线条颜色、字体族、字号、间距系数。官方内置了三套主题:默认浅色、深色(适合夜间分享和投屏)、极简黑白(适合打印)。用户也可以自定义主题文件,通过 JSON 覆盖任意变量。这个设计背后的考虑是:让团队可以维护统一的视觉规范,比如公司主题色、标准字体,然后所有图自动遵守——这比每个人手工调出五种蓝色要省心得多。主题适用场景特点默认浅色文档插图、评审材料对比度均衡,适用性广深色主题投屏展示、晚间工作深色背景,重点高亮突出极简黑白正式文档、打印去掉一切装饰,只留结构自定义主题团队规范、项目定制变量驱动,一处修改全局生效3. 从零跑通一个最小实现3.1 项目结构与模块划分这个项目我按照"解析—构建—布局—渲染—导出"五个阶段来组织代码,每一阶段对应一个独立模块,模块之间通过数据结构衔接,不共享可变状态。目录结构大致是这样的:diagram-design/ ├── src/ │ ├── parser/ // 文本语法解析,产出元素树 │ ├── model/ // 元素树的数据结构与校验逻辑 │ ├── layout/ // 布局算法,层内排序、力导向模拟 │ ├── render/ // 从布局结果生成 SVG 字符串 │ ├── theme/ // 主题定义与变量合并 │ └── exporter/ // 导出 SVG/PNG/列表文件的入口 ├── tests/ // 解析、布局、渲染的单元测试 └── examples/ // 自带示例,方便快速上手为什么要按阶段拆?因为这样做每个环节都能独立测试。解析阶段出问题时,不需要去查渲染代码;布局效果不好时,可以把同一棵树喂给不同布局算法,对比结果。这种流水线式的架构对工具类项目特别友好,也方便后来者往里面加新的图表类型——你只需要保证输出符合 model 层的数据结构就行。3.2 核心流程:从文本到元素树再到布局整个流程可以用一条主线串起来。第一步是解析文本,这一步的本质是"把有结构的字符串变成有结构的内存对象"。我的解析器没有用正规文法或者复杂的词法分析工具,而是自己写了一个简单的逐行扫描器,识别节点定义行、连接行、分组块和属性块。因为语法本身就刻意设计得很简单,手写解析器反而更好维护,出错时的报错信息也更容易写得贴心。解析得到元素树之后,要做一次数据校验。比如检查是否有节点被定义了两次、连接指向的节点是否都不存在、分组是否有循环嵌套。这一步很有必要,因为后续布局算法对数据的合法性很敏感,一个悬空的引用就能让布局过程直接崩溃,提前校验能把问题暴露在更友好的阶段。布局阶段是最有意思的。以层级布局为例,第一步是拓扑排序,确定每个节点所处的层级序号。接着遍历所有跨层连接,给它们做"虚拟节点"处理——跨层连线被拆成多段短线段,这样后续的排序过程可以把连线交叉最小化当做一个排序问题来解决。最后根据层高和节点大小计算每个节点的具体坐标。这些算法本身是经典图论算法,但在工程落地时,大量的时间花在调参上:层间距设多大、节点之间的水平间隙是多少、如何处理孤立节点不参与分层。3.3 渲染成 SVG 的几个关键细节当每个节点都有了坐标之后,渲染阶段就相对直接了。我会遍历元素树,将每一个节点渲染成一个 SVG 图形(矩形、圆角矩形或圆形,取决于类型设置),将连接渲染成带箭头的路径。这里有一个非常值得注意的细节:箭头。很多人画图时觉得箭头"差不多对着就行",但用程序画箭头时,如果箭头的尖端直接对准目标节点的中心点(图形中心),箭头会被节点方块盖住,看起来就像陷进了矩形里。解决方案是先计算出连线与目标节点边框的交点,再让箭头终止于这个交点。这一步需要根据节点的形状类型(矩形、圆形、圆角矩形)分别做几何计算。听起来简单,实际坑很多,因为你画的不光是直线,还有曲线路径,曲线的终点方向要靠当前切线方向来确定。另外一个细节是 Clip 和遮挡关系。当节点尺寸比较小、文字比较长时,文字会溢出边界。我的处理是给节点内的文本加上最大宽度限制,超出部分截断用省略号表示,鼠标悬浮时可以查看完整内容(在支持交互的环境下)。这个策略虽然牺牲了一点展示完整性,但保证了大图中不至于出现大量文字重叠遮挡的"烂图"。渲染完成后,把 SVG 元素拼接成字符串,写入文件即可。SVG 的 header 部分要注意加上xmlns命名空间声明,否则某些工具打开会直接报错或不显示。4. 实操中躲不开的坑4.1 自动布局那点"薛定谔的乱"自动布局算法在简单图上是惊艳的,在复杂图上就是薛定谔的乱——你永远不知道它这次会不会给你一个满意的结果。我自己被这个问题困扰了很久。后来总结出的经验是:不要和布局算法硬刚,要给它"引导"。布局算法最早只接受节点和边,后来我给 model 层加了几个非强制属性,比如"节点建议方向"(left/right)、"层级优先级""分组锚定"。用户可以在文本里写出:网关 { 首选层级: 1; } 订单服务 { 首选层级: 2; }布局算法在计算时会把首选层级作为强约束加入拓扑排序,这样用户就能通过微调几个关键节点,把整张图的结构拉整齐。这个"提示机制"比给每个节点设置绝对坐标要优雅得多,因为它的意图明确——用户干预的是逻辑结构,而不是像素位置。提示:遇到布局乱时,先别改算法参数。先试着把关键的"骨架节点"加上层级或方向提示,大多数情况下比调参更有效。4.2 中文、特殊字符与字体的隐藏问题如果你画的是中文图表,字体这块大概率会踩坑。最早做出来的 SVG 在电脑上打开很正常,但某次在同事的电脑浏览器里打开,所有中文都变成了方块,排查了很久才发现是字体回退的问题。SVG 里如果没有显式指定字体,渲染引擎会依赖系统默认字体,但不同系统默认字体差异很大(Windows、macOS、Linux 各不一样)。最终方案是在主题配置里增加几组字体栈设置:{ "fontFamily": "'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', sans-serif" }这样在不同系统上,都能优先匹配到系统中文字体,实现相对一致的显示效果。另外,如果图表文本里包含类似<、&这样的字符,在生成 SVG 时如果不做转义,会让整个 XML 解析出错。所有用户输入的内容我都强制经过一道 HTML 实体转义,这既是安全要求,也是 SVG 合法性的基本要求。4.3 大图场景下的性能与内存问题画一张二十个节点的图毫无压力,但当你试图用程序批量生成几百个节点的依赖全景图时,性能问题就冒出来了。力导向布局算法的时间复杂度大约为平方量级,每次迭代都要计算所有节点对之间的斥力,几百个节点时画画还算流畅,上千个节点时迭代一轮就可能是好几秒。我的处理办法是分三层优化。第一层是减少迭代次数——不再等到完全收敛,而是设定一个最大迭代轮次,并且在每轮结束时计算节点移动的平均距离,如果低于阈值就提前终止。第二层是空间分区——把画布切成网格,只计算相邻网格内的节点斥力,大幅减少无效计算。第三层是降低 SVG 复杂度——对于密集区域,用一条路径合并多条连线,减少 DOM 节点数量。这里有个很关键的取舍:大图的渲染质量首先保证的是"能看懂结构",而不是"每一个节点都精致"。几十个节点的小图,宽松间距和饱满样式是优点;几百个节点的大图上,同样的样式会让整张图变成一坨色块。我后来引入了"密度感知"的样式策略:当节点数量超过阈值时,自动缩小节点面积、压缩间距、去掉圆角和阴影,只用最小视觉元素表达结构。4.4 常见问题速查表现象可能原因解决办法中文渲染成方块缺少中文字体配置在主题中设置字体回退栈,覆盖常见系统中文字体导出的 SVG 在某些工具里打不开缺少 XML 命名空间或实体未转义检查<svg>头部的xmlns属性,并转义特殊字符节点顺序错乱、箭头乱指没有进行拓扑排序就进入布局先做拓扑排序,再处理孤立节点连线盖住节点文字箭头端点计算未处理节点边界计算连线与节点形状的边界交点,以此为终止点大型图渲染卡顿布局计算量过大、SVG 节点过多开启空间分区、减少迭代、使用密度感知样式手动调整的部分总被布局覆盖布局算法优先权高于手动提示检查是否有强制布局参数,保留手动锚点的最高优先级5. 从项目到日常工作的延伸5.1 让图表变成可协作、可评审的沉淀物做这个项目之后,我最大的感触是:图表一旦变成代码资产,它的使用方式就完全变了。以前大家在群里发架构图图片,遇到改动只能发一个新图,还要嘴说"以最新这张为准"。现在某项目把系统架构图写成了 diagram-design 的声明文件,放在仓库的 docs 目录里。有人改了服务依赖关系,提交的 diff 里能直接看到哪一行从服务A -> 服务B变成了服务A -> 服务C。评审的时候对着文件改,直观且不容易漏。这种模式下,图表的生命周期和代码完全同步了。自动构建时顺手渲染一张最新的架构图上传到内网知识库,任何时刻看到的那张图,都对应着仓库的某一 commit。我后来在一个团队内部推广这个流程时,重点强调的其实是这一句话:别再把图当图,把它当成"可视化视图"源文件,图的维护成本就能压缩到几乎为零。5.2 后续还可以扩展的方向如果说这个项目现在还有哪些明显值得做的改进,我列了三个方向。第一个是交互式浏览模式。现在的渲染结果是一张静态 SVG,但 SVG 本身是支持事件绑定的。可以做一个简单的预览页面,支持点击节点高亮相关链路、拖动画布、放大缩小、查看节点关联的属性详情。这些功能不会影响核心架构,只需要在渲染层输出带数据属性的 SVG 节点即可。第二个是模板化生成。很多图的拓扑结构其实是有规律的,比如前端项目里的依赖图、微服务调用链。可以抽几个常用模板,用户只需要填关键内容,就能生成一整套图。比如"微服务架构模板":网关层、业务层、数据层各放几个节点,再填依赖关系。第三个是集成到文档工作流的插件。如果文档系统支持嵌入自定义节点,就能直接把 diagram-design 的源文件嵌进页面,实现"文档里看到的图就是本地源文件渲染出来的"。这样图就和文档彻底绑定,不再有截图同步问题。最后再分享一个我在做这个项目时意外学到的小技巧:别急着从零实现布局算法。很多图表系统的核心价值不在算法本身,而在于语法设计、样式系统和工程化集成。把布局算法换成现成的开源库,完全不影响项目的独特价值,反而能帮你把精力聚焦在真正有差异的地方。做工具类项目尤其容易陷入"什么都要自己写"的冲动,但克制住这个冲动,产品反而走得更远。