最近在 GitHub 上看到一个热度涨得很快的项目 diagram-design第一眼看到它的示例输出时我确实愣了一下这是用代码生成的架构图居然不是设计师在 Figma 里手工摆出来的整个项目主打一个目标——用纯 HTML SVG 写出出版级质量的图解让程序员用自己最熟悉的技术栈做出能让设计师点头的架构图、拓扑图和流程图。我把源码完整拉下来读了一遍还把示例模板改造成了团队实际在用的系统架构图。这篇就把源码结构、核心实现和落地过程尽量拆开讲清楚。适合谁看被画图折腾到崩溃的开发写技术文档需要配图的人以及想把博客插图从“能看就行”提升到“能发出去见人”的写作者。1. 这个项目到底在解决什么问题1.1 程序员画架构图的三大尴尬画架构图这件事说起来轻巧做起来是真的烦。我自己经历过三个阶段每个阶段都有各自的问题。第一阶段是手工拖拽。打开通用绘图工具或者在线白板用鼠标一点点对齐方框和箭头。画一张二十个节点的系统图光对齐就要耗掉大半天。最痛苦的是中途改需求“把这个模块拆成两个微服务”“这里加一层消息队列”。听起来只是删两个框、加两个框实际上整张图的布局全乱了重新连线又是一轮痛苦。而且这类图是二进制或者私有格式存储的想放到 Git 里做版本对比基本做不到。过了两周回头看根本不知道这张图是什么时候改的、为什么改。第二阶段是文本式图表工具。Mermaid、Graphviz 这类方案解决了“可版本化”的痛点写几行声明式文本就能出图命令还能嵌入 CI 或者 Markdown 文档。但用久了会发现这类工具的默认风格非常固定节点类型有限对排版的控制力很弱。我想画一个数据库集群用圆柱体、缓存层用棱形、外部系统用云朵再给服务之间的连线加上不同颜色和线型传统文本图表工具要么做不到要么做出来的效果像 1998 年的网站。换句话说它能让你快速得到一张“图”但很难得到一张“好看且信息层次清晰”的图。第三阶段就是寻找“代码生成但样式高级”的方案。我试过用 D3.js 自己画精度上去了但学习成本高得离谱而且 D3 更擅长数据可视化做架构图还得自己实现布局策略、连线算法、端口吸附规则完全是重造轮子。我需要的其实是一个中间形态的东西既保留写代码的高效率和可复用性又能输出带有设计水准的矢量图。diagram-design 正好落在这一档上。1.2 diagram-design 的定位代码生成的出版级 SVGdiagram-design 看起来是 2024 前后在 GitHub 上活跃度上升的开源项目它把“画图”这件事重新定义了一遍不是用鼠标绘制而是用 HTML 结构和 JavaScript 配置去描述一张图的组成最终渲染结果不是位图而是纯 SVG 矢量图。我把项目示例下载下来之后对几个亮点印象很深。第一个是它的输出默认带一套完整的设计系统包括统一的间距栅格、语义化配色、节点阴影和圆角规则。哪怕你完全不改样式直接套用默认模板产出的图也比大多数手工拖拽出来的架构图要整齐。第二个是它的节点和连线都支持自定义样式和交互事件因为 SVG 的每个元素本质上都是 DOM选中的时候可以直接用 CSS 改样式也可以绑定事件做在线文档里的点击高亮。第三个是它在文字排版上花了很多心思文字换行、居中对齐、字体回退栈都处理得不错不像一些原生 SVG 工具那样出现中文乱跑对齐不齐的问题。要说它适合什么场景我实际体验下来觉得这几类最契合系统设计文档里的架构图、技术博客里的流程图和拓扑图、方案评审用的汇报插图以及需要印刷或者高清投屏的出版级图解。它的定位不是替代所有作图工具而是把“程序员用代码画一张高质量矢量图”这件事做到顺手。2. 源码核心思路拆解为什么偏偏是 HTML SVG2.1 选型背后的三个硬核理由读源码的时候我一直在想一个问题方案选型时摆着 Canvas、WebGL、SVG 多条路为什么 diagram-design 选择围绕 HTML SVG 做文章看完 renderer 模块之后我的结论是这三点起了决定性作用。首先是 SVG 的 DOM 本质。SVG 图有两种主流渲染路线Canvas 是像素画布画完就没了SVG 则保留一棵完整的元素树每个矩形、连线、文字都是可以单独访问的节点。这意味着你可以给某个节点加 title 实现悬浮提示可以给某条连线加 class 切换颜色还可以在文档里嵌入 SVG 后让用户用浏览器自带的搜索功能直接搜到图中的文字。对于技术文档和在线演示场景这几乎是无价的。其次是矢量输出本身的质量优势。出版级质量这个词听上去有点玄落到实际上就是两件事无限缩放不出马赛克以及印刷 300dpi 下边缘依然锐利。SVG 本身是文本格式存储和传输都轻量同时可以被代码压缩、可以做 diff。架构图放进 Git 仓库之后每次改动可以通过 diff 看到具体是哪个节点挪了坐标、哪条连线改了颜色这是 PNG 永远做不到的。第三是学习成本。diagram-design 的 DSL领域特定语言本质上就是 HTML 结构加 JavaScript 对象前端开发者上手几乎零门槛。它的 shape 系统可以理解成一个组件库你用调用函数的方式把预置图形渲染进 SVG 画布。不需要学习任何图形学基础也不需要掌握贝塞尔曲线理论就能做出基本体面的架构图。2.2 源码目录与工程结构项目源码组织得比较清爽不是那种几千行代码塞在一个文件里的玩具项目。我把主体结构整理成下面这样diagram-design/ ├── src/ │ ├── core/ │ │ ├── renderer.js # 渲染入口 │ │ ├── layout.js # 自动布局引擎 │ │ ├── geometry.js # 坐标与几何计算 │ │ └── validator.js # 配置校验 │ ├── shapes/ │ │ ├── index.js # 图元注册表 │ │ ├── rect.js │ │ ├── roundedRect.js │ │ ├── cylinder.js │ │ ├── diamond.js │ │ ├── cloud.js │ │ └── ... │ ├── palette.js # 颜色设计系统 │ └── export/ │ ├── svgOptimizer.js # SVG 代码精简 │ └── pngExporter.js # 位图导出 ├── examples/ │ ├── basic-architecture.js │ ├── network-topology.js │ └── flow-chart.js └── package.json核心代码全部集中在 src/core 三个文件里职责划分很明确。renderer.js 负责把数据配置翻译成 SVG 标签layout.js 负责计算每个节点该放在哪一层的哪个位置geometry.js 处理连线的起终点、弯折点和箭头偏移。shapes 目录下每个文件对应一种图形组件以标准化的接口注册到 index.js 里。这样的分层让扩展新图元变得非常简单——新写一个文件实现统一的 render 方法注册进来就能在配置里直接使用。我最喜欢的是源码里对“配置校验”的重视。validator.js 会在渲染前检查整个配置对象节点是否重名、坐标是否越界、连线引用的节点是否真实存在这些都会在控制台输出明确的错误信息。别小看这一步我自己在写复杂图的时候经常因为复制粘贴漏改 id 导致连线指向不存在的节点项目直接给出带行号的报错省了很长时间。2.3 渲染管线从数据到 SVG 都发生了什么diagram-design 的渲染流程可以用一条很清晰的管线来描述配置输入、布局计算、图形渲染、导出优化。第一步你传入一个描述图表结构的配置对象里面包含画布尺寸、节点列表、连线列表和样式覆写。第二步layout.js 读取这些节点和连线通过依赖关系推导出层级和坐标。第三步renderer.js 遍历处理后的节点和连线逐个调用 shapes 里注册的渲染函数生成对应的 SVG DOM 节点并挂载到根元素。第四步如果走 CLI 或者导出功能会经过 svgOptimizer.js 做标签精简去掉冗余属性、合并相同路径、压缩空白字符。核心渲染函数的逻辑大致是function render(diagram) { const layout autoLayout(diagram.nodes, diagram.edges); const svg createSvgRoot(diagram.width, diagram.height); for (const node of layout.nodes) { const shape getShape(node.shape); svg.appendChild(shape.render(node)); } for (const edge of layout.edges) { const path edgePath(edge, layout.nodes); svg.appendChild(renderEdge(path, edge.style)); } return optimizeSvg(svg); }这套实现的关键在于把“数据”和“坐标”解耦开。你在配置里写的是节点的逻辑关系layout.js 负责把逻辑关系映射成物理坐标。这样做的好处显而易见调整布局策略不需要改业务配置反过来你想固定某个节点的位置也可以通过配置直接指定自动布局会自动跳过已锁定坐标的节点。3. 关键模块逐段赏析从坐标计算到样式体系3.1 自动布局让节点自己找位置diagram-design 的自动布局没有直接用现成的图形布局库而是在 layout.js 自己实现了一套足够用的分层布局引擎。我翻源码时发现它处理的核心问题是三层以内的依赖分组根据节点之间的连线关系做拓扑排序计算出每个节点属于第几层然后按照层号决定 x 坐标按照同一层内的序号决定 y 坐标。function autoLayout(nodes, edges) { const layers computeLayers(nodes, edges); const positions []; layers.forEach((layer, layerIndex) { const x PADDING layerIndex * (LAYER_WIDTH GAP_X); layer.forEach((node, index) { const y PADDING index * (ROW_HEIGHT GAP_Y); positions.push({ ...node, x, y }); }); }); return positions; }为什么选这种确定性的规则而不是用力导向图这类物理模拟算法我揣摩了一下作者的意图关键在于“可预期性”。物理模拟算法适合探索式画图但同一个输入跑两次可能得到不同布局这在文档场景里是灾难。确定性规则每次生成结果完全一致方便 Git 做变更追踪也方便团队协作时讨论“这个节点应该往左移一点”。不过这个自动布局也有它的边界节点数量特别多或者依赖关系复杂的图出来的效果不一定是最优解。我的建议是三层以内让它跑超过三层就手动指定部分节点坐标或者拆分多张子图再嵌套引用。3.2 图元库为什么默认输出就有设计感shapes 目录是我读源码时看得最舒服的部分每个图形组件都遵循同一套接口看起来像工程化的“SVG 图元组件库”。拿 cylinder.js 举例数据库圆柱体并不是简单画一个椭圆加两条直线而是为了保证视觉精细度边缘用了 1px 的描边加轻微的渐变填充让圆柱体有体积感但又不至于花哨。图元的注册机制也很有参考价值const shapeRegistry new Map(); export function registerShape(name, shape) { shapeRegistry.set(name, shape); } export function getShape(name) { if (!shapeRegistry.has(name)) { throw new Error(Unknown shape: ${name}); } return shapeRegistry.get(name); }新增一个图元只需要实现 render 方法把 node 对象转成 SVG 字符串或 DOM 节点然后注册进去。内置图元从矩形、圆角矩形、圆柱体到云朵、角色、外部队列覆盖了画架构图时 90% 以上的需求。如果你需要更具体的图形比如 Kubernetes 的 Pod 图标或者 Kafka 的队列符号直接自定义一个 shape 并注册就能无缝接入。3.3 样式体系出版级质感的底层逻辑diagram-design 默认样式之所以讨喜关键是有一套完整的颜色语义表和统一的视觉参数。打开 palette.js 能看到颜色不是随便选的而是按照功能做了语义化命名export const palette { background: #ffffff, surface: #f8fafc, border: #cbd5e1, text: #0f172a, primary: #2563eb, success: #16a34a, warning: #d97706, danger: #dc2626, purple: #7c3aed };注意它把 border 和 text 都做成了低饱和度的中性色这就避免了那种默认节点黑框白底的“远古风”。在实际渲染时节点默认是 surface 底色加 primary 描边线条默认是 border 色只有强调关系的连线才会用 primary 之类的高亮色。这套逻辑很像设计系统里的“主次分明”视觉重心明确看图的人一眼就知道该关注哪里。另一个细节是圆角和阴影的克制。圆角统一设置为 6px阴影用的是低透明度而非纯黑色filter: drop-shadow(0 1px 2px rgba(15, 23, 42, 0.08))这种阴影只在边缘产生一点点柔和层次不会让整张图看起来脏。很多工具画出来的图显廉价就是因为阴影太重、颜色饱和度过高。diagram-design 默认帮你避开了这些坑。3.4 文本排版SVG 文字为什么难伺候如果你用原生 SVG 写过图肯定遇到过文本排版的各种问题文字宽度无法自动测量、换行得自己计算、垂直居中对齐在不同浏览器里表现不一致。diagram-design 在排版这一块专门做了封装值得细聊。SVG 的 text 元素不像 HTML 的 div 那样有自动换行能力所以项目在 geometry.js 里实现了一个字符宽度估算函数。它根据字体大小、字体族和中英文字符的差异估算出一段文字在该字号下大概占多少像素再结合节点宽度做切分实现近似换行function wrapText(text, maxWidth, fontSize) { const chars [...text]; const lines []; let currentLine ; for (const char of chars) { const charWidth isCJK(char) ? fontSize : fontSize * 0.55; const testLine currentLine char; const testWidth [...testLine].reduce((sum, c) sum (isCJK(c) ? fontSize : fontSize * 0.55), 0); if (testWidth maxWidth currentLine) { lines.push(currentLine); currentLine char; } else { currentLine testLine; } } if (currentLine) { lines.push(currentLine); } return lines; }这段代码虽然简单但解决了最核心的问题。配合 text-anchormiddle 和 dominant-baselinecentral能保证文字始终在节点正中显示。不过这里也提醒一句中文字符宽度和英文不同项目中针对 CJK 字符单独按全角计算宽度这种做法非常对路避免中文标题被截断或者溢出节点边界。4. 实操复现把默认模板改成你自己的系统架构图4.1 10 分钟跑起项目拉代码、安装依赖这一步没什么悬念git clone https://github.com/your-fork/diagram-design.git cd diagram-design npm install npm run dev项目内置了一个简单的本地预览服务默认会打开 examples 目录下的示例页。我第一次跑起来的时候页面已经在渲染一张服务网格架构图可以拖拽节点、点击连线高亮效果很直观。如果你是 CLI 重度用户它还提供了一个命令行的快捷入口npx diagram-design --input ./diagram.config.js --output ./output.svg输入一个 JS / TS 配置文件输出一个净化后的 SVG 文件。这个命令非常适合接进 CI 流程架构图跟着代码仓库走合并请求更新时自动重新生成最新图片。4.2 用配置对象定义一张组件图我不太习惯用 CLI 一步到位毕竟中间需要反复调参所以推荐在开发模式里改配置。下面的配置是我复现团队订单系统时写的简化版import { createDiagram } from diagram-design; const diagram createDiagram({ width: 1200, height: 800, grid: 8, nodes: [ { id: gateway, x: 60, y: 60, w: 220, h: 90, shape: roundedRect, label: API Gateway }, { id: auth, x: 360, y: 60, w: 200, h: 90, shape: roundedRect, label: Auth Service }, { id: order, x: 360, y: 220, w: 200, h: 90, shape: rect, label: Order Service }, { id: db, x: 360, y: 400, w: 220, h: 120, shape: cylinder, label: MySQL 主库 }, { id: mq, x: 650, y: 220, w: 180, h: 90, shape: diamond, label: MQ Cluster } ], edges: [ { from: gateway, to: auth, label: JWT }, { from: gateway, to: order, label: HTTP }, { from: order, to: db, label: 读写 }, { from: order, to: mq, label: 异步投递 } ] }); diagram.renderTo(#app);每个节点的字段都很直观id 是唯一标识x、y、w、h 定义位置和尺寸shape 决定图形类型label 显示文字。连线的 from、to 引用节点 id。运行之后布局引擎会自动检测依赖层级gateway 分到第一层auth 和 order 分到第二层db 和 mq 分到第三层整体层次关系一目了然。4.3 定制品牌色和节点图标默认样式好看是好看但放进公司文档里总觉得少点辨识度。项目提供了样式覆写机制你可以通过 createDiagram 的 theme 字段传入自定义的主题const diagram createDiagram({ ...config, theme: { palette: { primary: #4f46e5, surface: #f5f3ff, border: #c4b5fd }, shape: { roundedRect: { radius: 8, strokeWidth: 1.5 } } } });我实际测试下来theme 对象的优先级高于内置 palette覆盖之后所有节点的描边和主色都会同步变化不需要逐个节点去改。如果你想给某个节点单独加图标或品牌样式可以在节点配置里加 style 字段直接写 SVG 属性{ id: payment, shape: roundedRect, label: Payment Service, style: { fill: #ecfdf5, stroke: #059669, strokeWidth: 2 } }这种细粒度的控制能力是普通文本图表工具给不了的。你可以把最重要的服务节点描边加粗把处于异常状态的节点填充成警示色信息层次瞬间拉满。4.4 导出与嵌入博客/文档画完图之后导出和嵌入是最后一步。项目内置的导出功能支持 SVG、PNG以及在浏览器里打开交互式 HTML。我个人的习惯是优先导出 SVG因为它体积小、零失真还能在图里保留文本方便读者用浏览器搜索。嵌入博客或者公司 Wiki 时可以直接用 markdown 图片语法引用生成的 .svg 文件如果平台对 SVG 有安全限制无法展示再退一步导出 PNG。PNG 导出时会读当前画布的 viewBox按比例生成高清位图。如果你想在打印或者 PPT 里用建议把 config 里的 padding 调大一些防止边缘被截图工具裁掉。5. 常见问题与排查技巧实录5.1 常见问题速查表我在实操时踩了一些坑也帮群友排查过几个问题整理成一张速查表遇到相似情况可以直接对照现象可能原因解法导出 PNG 后边缘被裁掉画布 padding 太小节点贴边config 里增加全局 padding或手动给画布加 40px 留白中文文字显示为方块或乱码字体栈缺中文字体主题中设置 fontFamily 为 system-ui, Microsoft YaHei, sans-serif文字溢出节点边框节点宽度不够或换行函数被关闭调大节点 w或检查 labelStyle 里的 wrap 是否开启连线从节点上方穿过而不是绕行自动布局层级判定有误检查连线方向是否写反必要时手动指定节点坐标SVG 在博客中不显示平台过滤 SVG 的 script 或外链去掉自定义脚本把样式内联化改用 object 标签嵌入大量节点时页面卡顿每个节点都带阴影渲染开销大全局关闭阴影 effect或者拆分多张图5.2 三个独家避坑心得第一个心得是关于“尺寸单位别搞混”。SVG 支持 px、pt、em 多种单位但 layout.js 内部的计算默认按像素处理。如果你在配置里混用了单位比如宽 10cm、高 200px自动布局算位置时会把单位当普通数字做加法导致节点间距异常。所以尽量统一用数值默认当成 px不要把带单位的字符串写进坐标字段。第二个心得是“有向连线建议显式设置 direction”。项目自动布局会根据依赖关系推断连线方向但推断规则主要依据节点 id 的字母序和定义顺序。我遇到过一次 A 节点依赖 B 节点结果箭头画反最后发现是配置里 nodes 定义的先后顺序影响了推断。建议在 edge 配置里显式加 direction: forward 或 backward不要让引擎猜。第三个心得是“交互式 SVG 和静态 SVG 要分开导出”。项目在浏览器预览时有节点拖拽和点击高亮这些功能依赖 JavaScript 事件绑定。如果你把带这些绑定的 HTML 直接复制到文档里可能会因为事件冲突导致页面报错。正确的做法是导出静态 SVG 文件用于文档保留交互版仅用于演示环境。另外我在用 GitHub Actions 自动生成架构图时发现如果 commit 里同时改变了数据结构定义和 SVG 输出diff 会变得很占屏幕。合理的流程是先把配置改动合入再让 CI 重新生成图片分两次提交代码评审的人会感谢你的。6. 一个真实的使用场景复盘最后分享一个我实际落地的场景。团队两个月前重构了订单模块架构从单体改成微服务拆分架构文档里那几张图一直是从旧文档里复制出来的拓扑关系早就不对了。我花了一下午时间用 diagram-design 把这些图全部重画了一遍包含网关、鉴权、四五个业务服务、MySQL 主从、Redis 缓存、MQ 集群以及它们之间的调用关系。过程中没有手动拖拽过一个像素全部是通过配置和主题参数实现的。最终生成的 SVG 文件放进文档系统之后组里后端同事都以为是从设计工具里专门排版过的。对我个人来说这个项目最大的价值不是省了画图那半小时而是让架构图第一次具备了“代码资产”该有的特性可版本控制、可复用、可自动生成。后续每次服务拆分我只需要改配置、跑生成、提 PR架构图会和对应的代码变更记录绑在一起文档永远不会过期。如果你也被“画图十分钟、对齐全半天”折磨过不妨把 diagram-design 拉下来试试。这大概是目前我在“工程化出图”这件事上见过少有的兼顾易用和质感的解决方案。
