最近在 GitHub 上看到一个热度涨得很快的项目 diagram-design,第一眼看到它的示例输出时我确实愣了一下:这是用代码生成的架构图?居然不是设计师在 Figma 里手工摆出来的?整个项目主打一个目标——用纯 HTML + SVG 写出出版级质量的图解,让程序员用自己最熟悉的技术栈,做出能让设计师点头的架构图、拓扑图和流程图。我把源码完整拉下来读了一遍,还把示例模板改造成了团队实际在用的系统架构图。这篇就把源码结构、核心实现和落地过程尽量拆开讲清楚。适合谁看:被画图折腾到崩溃的开发,写技术文档需要配图的人,以及想把博客插图从“能看就行”提升到“能发出去见人”的写作者。
1. 这个项目到底在解决什么问题
1.1 程序员画架构图的三大尴尬
画架构图这件事,说起来轻巧,做起来是真的烦。我自己经历过三个阶段,每个阶段都有各自的问题。
第一阶段是手工拖拽。打开通用绘图工具,或者在线白板,用鼠标一点点对齐方框和箭头。画一张二十个节点的系统图,光对齐就要耗掉大半天。最痛苦的是中途改需求:“把这个模块拆成两个微服务”“这里加一层消息队列”。听起来只是删两个框、加两个框,实际上整张图的布局全乱了,重新连线又是一轮痛苦。而且这类图是二进制或者私有格式存储的,想放到 Git 里做版本对比,基本做不到。过了两周回头看,根本不知道这张图是什么时候改的、为什么改。
第二阶段是文本式图表工具。Mermaid、Graphviz 这类方案解决了“可版本化”的痛点,写几行声明式文本就能出图,命令还能嵌入 CI 或者 Markdown 文档。但用久了会发现,这类工具的默认风格非常固定,节点类型有限,对排版的控制力很弱。我想画一个数据库集群用圆柱体、缓存层用棱形、外部系统用云朵,再给服务之间的连线加上不同颜色和线型,传统文本图表工具要么做不到,要么做出来的效果像 1998 年的网站。换句话说,它能让你快速得到一张“图”,但很难得到一张“好看且信息层次清晰”的图。
第三阶段就是寻找“代码生成但样式高级”的方案。我试过用 D3.js 自己画,精度上去了,但学习成本高得离谱,而且 D3 更擅长数据可视化,做架构图还得自己实现布局策略、连线算法、端口吸附规则,完全是重造轮子。我需要的其实是一个中间形态的东西:既保留写代码的高效率和可复用性,又能输出带有设计水准的矢量图。diagram-design 正好落在这一档上。
1.2 diagram-design 的定位:代码生成的出版级 SVG
diagram-design 看起来是 2024 前后在 GitHub 上活跃度上升的开源项目,它把“画图”这件事重新定义了一遍:不是用鼠标绘制,而是用 HTML 结构和 JavaScript 配置去描述一张图的组成;最终渲染结果不是位图,而是纯 SVG 矢量图。
我把项目示例下载下来之后,对几个亮点印象很深。第一个是它的输出默认带一套完整的设计系统,包括统一的间距栅格、语义化配色、节点阴影和圆角规则。哪怕你完全不改样式,直接套用默认模板,产出的图也比大多数手工拖拽出来的架构图要整齐。第二个是它的节点和连线都支持自定义样式和交互事件,因为 SVG 的每个元素本质上都是 DOM,选中的时候可以直接用 CSS 改样式,也可以绑定事件做在线文档里的点击高亮。第三个是它在文字排版上花了很多心思,文字换行、居中对齐、字体回退栈都处理得不错,不像一些原生 SVG 工具那样出现中文乱跑对齐不齐的问题。
要说它适合什么场景,我实际体验下来觉得这几类最契合:系统设计文档里的架构图、技术博客里的流程图和拓扑图、方案评审用的汇报插图,以及需要印刷或者高清投屏的出版级图解。它的定位不是替代所有作图工具,而是把“程序员用代码画一张高质量矢量图”这件事做到顺手。
2. 源码核心思路拆解:为什么偏偏是 HTML + SVG
2.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-anchor="middle" 和 dominant-baseline="central",能保证文字始终在节点正中显示。不过这里也提醒一句:中文字符宽度和英文不同,项目中针对 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 拉下来试试。这大概是目前我在“工程化出图”这件事上见过少有的兼顾易用和质感的解决方案。