ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SVG架构图绘制指南:从出版级排版到代码化维护

SVG架构图绘制指南:从出版级排版到代码化维护 最近在GitHub热榜上刷到一个项目叫diagram-design讨论热度不低。点进去看了下源码发现它不是什么花哨的图表库而是用纯HTMLSVG把架构图、流程图这种技术图示做到“出版级”的一个开源方案。这个定位很有意思因为大部分人画架构图要么用draw.io随便拖要么拿Figma手搓最后导出一张静态图片往文档里一贴。图是能看懂但线条对不齐、间距不统一、想改个颜色得重新画离“设计师也认可”这个标准差得远。这个项目解决的痛点就是把架构图变成“代码产物”用SVG矢量图渲染每一根线、每一个色块都能在源码里精确定位不仅能批量生成还能走Git版本管理。对做技术文档、写博客、维护架构说明的人来说这比维护一张图片靠谱得多。这篇就结合我读源码时的理解聊聊diagram-design的核心设计、实际怎么用以及我踩过的几个坑。1. 内容整体设计与思路拆解1.1 为什么SVG会胜出从渲染原理说起先看一个关键问题市面上的图表方案很多Mermaid、PlantUML、ECharts、Graphviz凭什么要选HTMLSVG我的判断是diagram-design选SVG核心理由是“它和HTML同源”。SVG本身就是XML语法放进浏览器里就是DOM的一部分所以写HTML时用的CSS、JavaScript在SVG上全都适用。这带来了几个很现实的好处主题可以通过CSS变量换交互可以直接绑事件文字可以直接被搜索引擎检索到更重要的是它可以被脚本批量生成。这一点恰恰是Mermaid、PlantUML做不到的——它们也能用文本描述图但生成的是渲染好的位图或内嵌结构拿到手上改动不如SVG灵活。再说SVG和Canvas的区别。Canvas是“画布”模式画完就是像素没法单独选中某个节点的DOM去改样式SVG是“保留模式”每个矩形、每条路径都是独立节点随时可以操作。对架构图这种需要反复调整的场景来说SVG的保留模式天然合适。何况现在显示器都是高分屏SVG矢量渲染出来的线条在缩放时不会糊导出PDF印刷也没问题。1.2 出版级图解到底指什么排版的基本功光有SVG不代表图好看diagram-design真正值钱的地方是把平面设计里那套排版规则落到了代码里。我拆了一下它的源码发现它对“出版级”的定义集中在三个东西上字阶、间距、对齐。字阶就是字号的比例关系标题用多大、标签用多大、注释用多大不能随意拍脑袋。它用的近似1.333倍的大三度音程18px的标题、14px的正文标签、12px的注释视觉层次非常舒服。间距走的是8px基准网格所有内边距、外边距、元素间距都是8或8的倍数整体布局就有呼吸感。对齐更严格所有节点边界贴齐在同一组网格线上而不是“肉眼看差不多”。在SVG里实现这个就是用整数坐标、统一矩形尺寸然后让脚本计算每个节点坐标而不是靠人眼拖动。2. 核心细节解析与实操要点2.1 从坐标系统到第一段SVG动手写之前得先弄懂SVG的几个基础概念不然源码读起来一头雾水。最核心的是viewBox它定义的是一个“虚拟画布坐标”配合width和height使用。比如一个图的实际尺寸是1200x800但我想让它在不同容器里等比缩放就可以写成svg viewBox0 0 1200 800 preserveAspectRatioxMidYMid meetpreserveAspectRatio控制的是缩放时怎么保比例meet表示完整显示、不裁剪。这套机制相当于设计稿里的标注系统我在代码里永远按1200x800这个坐标系设计浏览器自动帮我缩放适配。有了画布画节点其实就是画矩形加文字。diagram-design里一个典型节点是这样的g transformtranslate(60, 40) rect width180 height64 rx8 fill#f8fafc stroke#94a3b8 / text x90 y32 text-anchormiddle dominant-baselinemiddle font-size14Nginx 负载均衡/text /g这里用g做了分组transform把整个组“挪”到指定位置组内部坐标就变成相对坐标后续复用起来非常方便。2.2 组件化复用的设计逻辑defs和usediagram-design源码里大量出现defs和use这对组合这是SVG高效复用的关键。思路和编程里的组件是一模一样的先把“长什么样的节点”定义好再在需要的地方“实例化”。defs g idnode-box rect width180 height64 rx8 fillinherit strokeinherit / text x90 y32 text-anchormiddle dominant-baselinemiddle font-size14 classnode-label/text /g /defs然后这样实例化use href#node-box x60 y40 fill#e2f0fa stroke#4a90d9 /注意我没有在g idnode-box里写死fill而是用fillinherit让外部通过use上的属性去覆盖。这种“定义抽象组件、实例传入参数”的模式和React、Vue组件传props是一个逻辑代码写起来很清爽也方便脚本批量填充数据。在浏览器里defs里的内容会被缓存重复实例化性能开销也小。2.3 连接线和曲线控制点让图不生硬的关键连接线是架构图最容易显粗糙的地方。很多人画连线直接用直线交叉起来乱成蜘蛛网。diagram-design用的是贝塞尔曲线三次贝塞尔在视觉上比直线柔和得多。一条连接线的path是这样path dM 240 72 C 300 72, 340 112, 400 112 fillnone stroke#94a3b8 stroke-width1.5 /这段path的意思是从(240,72)这个点开始用两个控制点(300,72)和(340,112)画一条曲线到(400,112)。控制点的位置决定了曲线弯曲的方向和弧度。它的源码里会做一个计算根据起点节点和终点节点的相对位置自动算控制点偏移量。大致逻辑是如果终点在起点右侧偏下那么起点控制点往右偏移一段距离终点控制点往左偏移一段距离偏移量等于两点水平距离的一半但设一个最小值。这样不管两个节点位置怎么变连线都能自然弯曲不会交叉得很突兀。箭头则是用SVG的marker做的定义一个箭头形状然后在path上引用defs marker idarrow viewBox0 0 10 10 refX8 refY5 markerWidth6 markerHeight6 orientauto-start-reverse path dM 0 0 L 10 5 L 0 10 z fill#94a3b8 / /marker /defs path d... marker-endurl(#arrow) /3. 实操过程与核心环节实现3.1 拉下代码和初始化项目这块是我实际跑的过程。假设你已经配好Node环境直接克隆git clone https://github.com/你的仓库/diagram-design.git cd diagram-design npm install npm run dev项目结构大概是这样的diagram-design/ ├── src/ │ ├── components/ # 可复用SVG节点组件 │ ├── layout/ # 自动布局和坐标计算 │ ├── theme/ # CSS变量主题 │ └── render/ # 渲染逻辑 ├── examples/ # 示例图 └── docs/真正核心的是layout和theme两个目录。layout负责把节点列表和连线关系计算成坐标theme提供CSS变量做样式定制。3.2 用数据驱动方式画一张部署架构图现在用一个实际场景复现我要画一个简单的部署架构图包含三个节点——Nginx负载均衡、应用服务、MySQL数据库两层连线。传统做法是手动拖拽对齐这里我写一份数据让diagram-design自动算坐标。const nodes [ { id: nginx, label: Nginx 负载均衡, x: 60, y: 40, w: 180, h: 64, color: #e2f0fa, border: #4a90d9 }, { id: app, label: App Server, x: 60, y: 160, w: 180, h: 64, color: #e6f7e6, border: #3da93d }, { id: db, label: MySQL 主从, x: 60, y: 280, w: 180, h: 64, color: #fde9d9, border: #d9822b } ]; const edges [ { source: nginx, target: app }, { source: app, target: db } ];渲染函数大致是这样的逻辑遍历nodes用use实例化节点组件遍历edges根据两端节点坐标计算贝塞尔曲线的path。function render(svg, nodes, edges) { const defs svg.querySelector(defs); nodes.forEach(n { const use document.createElementNS(http://www.w3.org/2000/svg, use); use.setAttribute(href, #node-box); use.setAttribute(x, n.x); use.setAttribute(y, n.y); use.setAttribute(fill, n.color); use.setAttribute(stroke, n.border); svg.appendChild(use); }); edges.forEach(e { const source nodes.find(n n.id e.source); const target nodes.find(n n.id e.target); const d routeEdge(source, target); // 创建path并append到svg... }); }关键在routeEdge这个函数function routeEdge(source, target) { const sx source.x source.w; const sy source.y source.h / 2; const tx target.x; const ty target.y target.h / 2; const dx Math.abs(tx - sx); const offset Math.max(40, dx / 2); return M ${sx} ${sy} C ${sx offset} ${sy}, ${tx - offset} ${ty}, ${tx} ${ty}; }这里计算的是从source节点右边中间位置到target节点左边中间位置的三次贝塞尔路径。控制点往右、往左各偏offset曲线就会有一个自然的上拱或下探弧度。实测下来这种写法在节点上下错开、左右错开时都能保持弧线优雅不会出现抽筋式扭曲。3.3 主题定制和导出高清PNGdiagram-design的主题系统用的是CSS变量改起来非常直接:root { --diagram-bg: #ffffff; --node-bg: #f8fafc; --node-border: #94a3b8; --node-text: #0f172a; --line-color: #64748b; --accent-color: #2563eb; }只要换一套变量值整套图就换了视觉风格。这在维护多套主题时非常高效比如白天版、夜间版或者不同客户的不同品牌色。导出PNG也是一个实用性很高的点。SVG在浏览器里可以直接用XMLSerializer序列化成字符串然后通过Image对象画到canvas上再导出PNGconst svgData new XMLSerializer().serializeToString(svgElement); const canvas document.createElement(canvas); canvas.width 1200; canvas.height 800; const ctx canvas.getContext(2d); const img new Image(); img.onload () { ctx.drawImage(img, 0, 0); canvas.toBlob(blob { // 下载blob }); }; img.src data:image/svgxml;charsetutf-8, encodeURIComponent(svgData);需要注意如果SVG里用了外部字体canvas导出时字体可能没加载完导致文字位置错乱。我后来的做法是把关键文字在导出前先转成path或者确保字体完全加载后再导出。4. 常见问题与排查技巧实录4.1 中文换行和文字溢出SVG的text元素是不自动换行的这是很多新手第一个踩的坑。中文还好字符宽度和字号比例接近1:1但英文和数字就不一样了。diagram-design里做了一个wrapText函数思路是拿到每个字符的宽度逐个累加超过最大宽度就换行换行用tspan实现function wrapText(textEl, text, maxWidth) { const chars text.split(); let line ; let tspan document.createElementNS(http://www.w3.org/2000/svg, tspan); for (const ch of chars) { if (getTextWidth(line ch) maxWidth) { tspan.textContent line; textEl.appendChild(tspan); line ch; tspan document.createElementNS(http://www.w3.org/2000/svg, tspan); tspan.setAttribute(x, textEl.getAttribute(x)); tspan.setAttribute(dy, 1.2em); } else { line ch; } } tspan.textContent line; textEl.appendChild(tspan); }注意给换行tspan设置dy1.2em这样每行之间的距离是12px阅读起来不拥挤。4.2 部分浏览器的文字基准线偏移写SVG时有个跨浏览器兼容的坑dominant-baselinemiddle在某些旧版Firefox上支持得不好文字会往上或往下偏几个像素。我的处理办法是不依赖这个属性而是手动算y坐标文字高度是14px那么让文字中心对齐矩形中心就把y设成节点y 节点高度/2 5其中5是约等于0.35em的视觉补偿值。实测下来这样在不同浏览器里的表现一致很多。4.3 复杂图渲染卡顿的优化路径当节点数量超过一两百个、连线复杂时纯SVG渲染会出现卡顿。diagram-design的做法有几个方向可以借鉴尽可能把重复的路径合并把静态内容放进defs让浏览器缓存不要在循环里频繁设置fill、stroke属性合并成一次class切换。还有一个很有效的手段用requestAnimationFrame把渲染拆成帧避免一次同步渲染阻塞主线程。我做压力测试时画了一张200个节点的拓扑图优化前DOM节点数接近2万滑动页面明显掉帧。优化后把共用的背景网格、装饰元素全部收进defs再把不需要交互的节点加pointer-eventsnone掉帧问题基本消失。4.4 GitHub上预览SVG的兼容性问题这可能是把这个项目用于博客、README时最容易被忽略的地方。GitHub的README渲染对SVG做了一定过滤use xlink:href这种写法有兼容风险有时候会显示空白。我踩过一次坑后来把外链的href直接改成纯href、避免引用外部资源才恢复正常。如果你要把图发布到GitHub Pages建议先在本地预览一遍再推上去避免SVG被安全策略拦截之后图直接消失。5. 适用场景边界与扩展思路5.1 适合用diagram-design的几类场景从我实际使用的经验来看这类纯代码SVG方案非常适合下面这些场景技术文档和架构说明图随代码走任何一次改动都能在PR里被追踪不会出现“文档里的图和代码已经对不上”的问题。需要批量生成的图比如系统有几十个服务每个服务的调用关系是动态变化的用脚本生成SVG远比手工改图高效。需要多语言、多主题的场景SVG的text节点可以直接被i18n脚本替换主题由CSS变量控制发布时一次性生成多套版本。对输出质量要求高的内容比如技术图书、白皮书、对外分享材料这几类内容对图示的排版、字体、清晰度都有要求SVG矢量格式在这些场景本来就占优势。它和表格化工具draw.io、Figma最大的不同倒是它的短板反而成了优势没有可视化拖拽界面操作门槛高一些但也正因为一切都在代码里质量可控、可复用、可审阅。说白了它适合“把画图当写代码”的人不适合只想快速拖一张草图的人。为了看得清楚我整理了一个对比表格方案维护方式排版可控性自动化程度上手难度适用场景draw.io / ProcessOn图形文件中等靠手动对齐低只能手工导出低临时草图、快速沟通Mermaid / PlantUML文本描述弱无法细调像素级中文本生成低简单流程图、时序图Figma / Sketch设计文件高但和代码割裂低手动流程高复杂视觉稿、演示图diagram-design (HTMLSVG)代码高像素级控制高可脚本生成中长期维护的技术图、出版级图5.2 从静态图到可交互图表的扩展diagram-design做的全是静态输出但这个项目的底座毕竟是SVG天然适合做交互扩展。我基于它的源码改造过一个版本给节点加click事件点击后弹出详情面板给路径加mouseenter事件悬停时高亮关联链路还尝试过用CSS animation给数据流加动态虚线动画。代码上只需在创建节点和path时绑定事件use.addEventListener(click, () { showDetailPanel(node.id); }); path.addEventListener(mouseenter, () { path.setAttribute(stroke, #2563eb); path.setAttribute(stroke-width, 3); }); path.addEventListener(mouseleave, () { path.setAttribute(stroke, #94a3b8); path.setAttribute(stroke-width, 1.5); });这样就把静态架构图变成了一个轻量级交互原型。如果是做技术汇报这种动态高亮能比纯静态图多传达好几倍的信息量。5.3 什么情况不建议用纯SVG硬抗也不是所有图都适合用SVG硬写。节点数量上千、布局逻辑极其复杂的实时数据拓扑这种场景用SVG会吃力还是得考虑Canvas或者WebGL的库。另外如果团队里没人愿意维护代码纯粹就是想把图快速画出来发给别人看那直接用在线白板工具更省钱省力。diagram-design这类项目适合的是“图的生命周期比画图动作长”的场景比如一套架构图要在项目里活很多年持续被改动那代码化的价值就全出来了。最后分享一个小技巧我自己在实际使用中最受益的一个做法是用数据驱动的方式维护图的同时把坐标计算封装成纯函数写单元测试。比如routeEdge这个函数输入两个节点的坐标输出一条path字符串完全适合测试。我在测试里覆盖了“目标节点在左上方、左右对齐、水平居中”等几种典型情况确保每次改动节点布局或样式连线逻辑不会回归。这个习惯帮我省了很多排查时间因为图这东西视觉上出了问题经常是最难定位的。如果你打算长期用SVG维护架构图建议也试试这个思路比画完图人肉检查要靠谱得多。
返回列表