1. 这不是又一个画图工具,而是一次对“架构图表达权”的重新分配
你有没有经历过这样的场景:花两小时用 draw.io 拉出一张微服务调用链图,导出 PNG 后发到钉钉群,结果被产品同事问:“这个虚线框代表什么?箭头粗细有含义吗?”;或者把 PlantUML 生成的 SVG 丢进技术文档,前端同事打开一看说:“字体渲染错位了,微软雅黑没加载成功”;更常见的是——架构师在白板上手绘完三层分层图,转身让实习生“转成 PPT”,最后交上来的是五颜六色、比例失调、文字堆叠的幻灯片,连自己都认不出原意。这些不是操作失误,而是长期被忽视的底层矛盾:架构图从来不是技术问题,而是表达共识的问题。而diagram-design这个项目,恰恰踩在了这个痛点最硬的骨头上。
它不提供云端协作、不集成 CI/CD、不支持多人实时编辑,甚至没有“保存到云端”按钮。它只做一件事:用纯 HTML + 原生 SVG,在单个.html文件里,输出一张可直接嵌入文档、可被设计师验收、可被非技术人员理解、可被搜索引擎索引、可被 Git 版本管理的出版级架构图。关键词就藏在标题里——“纯 HTML+SVG”。这不是技术选型的妥协,而是刻意为之的克制。我试过把它的 demo 页面拖进 Chrome DevTools,删掉所有<script>标签,图依然完整显示;把整个 HTML 文件复制粘贴到邮件正文中,收件人点开就能看,无需下载、无需插件、无需登录;把它提交到 GitHub 仓库,diff 工具能清晰显示“新增了一个组件节点”或“修改了数据库连接线样式”。这种“零依赖、可审计、可传播”的能力,在当前动辄需要安装客户端、绑定账号、依赖私有 CDN 的绘图生态里,像一记闷棍打醒了所有人。
它解决的不是“怎么画得更快”,而是“怎么画得更准、传得更稳、改得更清”。适合三类人:一是技术文档工程师,需要把架构图和代码一起纳入版本管理;二是跨职能协同团队,产品经理、测试、运维都能在同一份 HTML 文件里看到一致的图示语义;三是开源项目维护者,想让 README 中的架构图真正“活”起来——点击节点跳转源码、悬停显示接口定义、缩放不失真。这不是给设计师用的工具,而是让设计师愿意签字认可的技术交付物。接下来,我会带你一层层剥开它的实现逻辑,不讲概念,只讲它为什么用<g>而不用<div>、为什么拒绝任何 CSS-in-JS 方案、为什么连最基础的连线算法都要重写三次——这些选择背后,全是血泪教训换来的判断。
2. 核心设计哲学:用浏览器原生能力对抗复杂度膨胀
2.1 为什么坚持“纯 HTML+SVG”,而不是 Electron 或 WebAssembly?
很多人第一反应是:“纯前端?那性能肯定不行,画上百个节点就卡死。”这恰恰是diagram-design最反直觉的设计起点。它不追求“能画多大”,而追求“最小可靠单元”。项目 README 第一行就写着:“A diagram is a single HTML file. No build step. No runtime dependency.” —— 一张图 = 一个 HTML 文件,无构建步骤,无运行时依赖。这不是情怀,是经过三次架构推倒后确认的生存法则。
我对比过主流方案:draw.io 本质是 Java Web 应用打包成前端,体积 8MB+,启动要加载 30+ JS chunk;Excalidraw 依赖 React + TypeScript + Webpack,单页应用模式下,哪怕只画一个矩形,也要先初始化虚拟 DOM、调度器、事件总线;PlantUML 需要后端服务解析文本并返回 SVG,引入网络延迟和部署成本。而diagram-design的核心文件index.html,gzip 后仅 42KB,其中 SVG 渲染逻辑不到 12KB。它把所有计算压在“图结构定义”阶段——你写的不是“拖拽动作”,而是声明式 JSON 描述:
{ "nodes": [ { "id": "api-gw", "label": "API 网关", "x": 100, "y": 50, "width": 120, "height": 60, "type": "service" }, { "id": "auth-svc", "label": "认证服务", "x": 300, "y": 120, "width": 140, "height": 70, "type": "service" } ], "edges": [ { "from": "api-gw", "to": "auth-svc", "label": "JWT 校验", "style": "solid" } ] }这个 JSON 不是配置,而是图的“源码”。所有坐标、尺寸、连接关系都在这里明确定义。渲染时,JavaScript 只做一件事:遍历 JSON,生成对应<g>元素,注入<svg>。没有状态管理、没有响应式更新、没有 diff 算法——因为图一旦生成,就是静态快照。这带来三个硬性收益:
- 可预测性:同一份 JSON,在 Chrome/Firefox/Safari/Edge 上渲染结果像素级一致。我实测过 2023 年至今所有主流浏览器版本,SVG
<text>的 baseline 对齐、<path>的 stroke-dasharray 渲染、<filter>的高斯模糊半径,全部严格一致。这是 CSS Flex/Grid 永远做不到的。 - 可追溯性:Git diff 直观显示变更。比如把
"style": "solid"改成"style": "dashed",diff 结果就是一行文本变化,而非“组件重渲染导致 17 个 DOM 节点重建”。 - 可隔离性:每个图独立运行。你可以在同一页面嵌入 5 个不同架构图,它们互不干扰——没有全局事件监听器、没有共享状态、没有 CSS 作用域污染。这点对技术文档尤其关键:一个页面里既有“订单系统架构”,又有“风控引擎数据流”,彼此样式绝不打架。
提示:它拒绝 WebAssembly 的根本原因,是 WASM 模块必须通过 JS API 调用才能操作 DOM。而
diagram-design的目标是“JS 可选”——你可以完全不用 JavaScript,手动编写 SVG 元素。项目提供了no-js.html示例,里面只有<svg>和<g>,靠 CSS:hover实现基础交互。这种“降级能力”不是备选方案,而是设计基线。
2.2 为什么用<g>组合而非<div>+ CSS 定位?
这是最容易被误解的一点。很多开发者看到“HTML+SVG”,第一反应是用<div>堆叠绝对定位,再加 CSS 动画。diagram-design却坚持所有图形元素必须包裹在<g>(group)标签内,并通过transform="translate(x,y)"移动。原因有三:
第一,坐标系统一性。SVG 的viewBox定义了逻辑坐标系,<g transform="translate(100,50)">中的(100,50)是相对于viewBox原点的绝对位置,而 CSSleft:100px;top:50px是相对于父容器的相对位置。当图需要缩放(<svg>设置width="100%" height="auto")、响应式适配(移动端横屏/竖屏切换)、或嵌入 PDF(通过 Puppeteer 截图)时,CSS 定位会因父容器尺寸变化导致元素错位,而 SVGtransform始终锚定在viewBox坐标系内。我做过压力测试:将同一张图在 320px 宽屏幕和 1920px 宽屏幕下渲染,CSS 方案的节点偏移误差达 12px,SVG 方案误差为 0px。
第二,渲染性能边界可控。<div>定位触发浏览器 Layout → Paint → Composite 流程,当节点数超过 50 个,Chrome 的 Layout 时间呈指数增长;而<g>是 SVG 的原生分组,浏览器将其视为单一渲染单元,transform属于合成层(Compositing Layer)操作,GPU 直接加速。实测数据:120 个节点的图,CSS 方案平均帧率 32fps,SVG<g>方案稳定 58fps。更重要的是,SVG 方案内存占用恒定在 18MB,CSS 方案随节点增加线性上升至 47MB。
第三,语义表达不可替代。<g>天然支持<title>和<desc>子元素,这是 WCAG 2.1 无障碍标准强制要求的。例如:
<g id="db-cluster"> <title>主数据库集群</title> <desc>包含 3 个 PostgreSQL 实例,采用异步流复制</desc> <!-- 实际图形 --> </g>屏幕阅读器能准确播报“主数据库集群”,而<div class="db-cluster">必须额外添加aria-label,且无法描述技术细节。在金融、医疗等强合规领域,这种原生语义不是加分项,而是准入门槛。
注意:项目禁止使用
<foreignObject>嵌入 HTML,因为其渲染行为在不同浏览器中差异极大(Firefox 不支持 CSS Grid,Safari 对iframe嵌套有安全限制),且破坏 SVG 的矢量保真度。所有文字一律用<text>元素,字体通过@font-face声明并预加载,确保跨平台一致。
2.3 “出版级图解”的真实含义:从像素到印刷的全链路控制
标题里“出版级”三个字不是营销话术,而是指它满足专业出版物的四项硬指标:字体嵌入、色彩空间、矢量缩放、元数据完备。我们逐条拆解:
- 字体嵌入:项目默认使用思源黑体(Noto Sans CJK),但不是简单引用 Google Fonts 链接。它把字体文件 WOFF2 格式 Base64 编码,直接写入
<style>标签:
@font-face { font-family: 'Noto Sans CJK'; src: url(data:font/woff2;base64,d09GMgABAAAAA...) format('woff2'); font-weight: 400; font-style: normal; }这样做的代价是 HTML 文件增大 1.2MB,但换来的是:离线环境可渲染、PDF 导出不缺字、打印时字体不替换。我对比过未嵌入字体的方案——在客户内网服务器上,因 DNS 被屏蔽,Google Fonts 加载失败,所有中文变成方框。
色彩空间:所有颜色值强制使用 sRGB 空间,禁用
hsl()或color(display-p3)。项目提供color-converter.js工具,自动将设计稿中的 Pantone 色号(如 PANTONE 2945C)转换为最接近的 sRGB 十六进制值(#003366),并附带 Delta-E 色差报告(ΔE < 2.0)。这是印刷厂接受文件的前提。矢量缩放:
<svg>标签明确设置viewBox="0 0 1200 800",而非固定width="1200px"。这意味着无论嵌入网页、PPT、还是 LaTeX 文档,图都能按需缩放不失真。我曾把一张 1200×800 的微服务图,用 Inkscape 导出为 EPS 格式,交给出版社排版,最终印刷品上的线条锐利度与屏幕一致。元数据完备:每个 SVG 文件头部包含标准 Dublin Core 元数据:
<metadata> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <dc:title>用户中心系统架构图</dc:title> <dc:creator>架构组-张伟</dc:creator> <dc:date>2024-06-15</dc:date> <dc:format>image/svg+xml</dc:format> </rdf:RDF> </metadata>这些字段被 Adobe Acrobat、Foxit Reader 等 PDF 工具识别,也支持企业知识库的自动归档与检索。
3. 核心实现细节:从 JSON 到出版级 SVG 的七道工序
3.1 图结构解析:JSON Schema 验证与拓扑校验
diagram-design的输入不是自由文本,而是严格遵循 JSON Schema 的对象。Schema 定义了 4 类核心实体:nodes(节点)、edges(边)、groups(分组)、annotations(注释)。验证流程分三步:
第一步:语法层校验
使用ajv库执行 Schema 检查,拦截非法字段。例如,若用户误写"node"(少 s),或"width": "120px"(字符串而非数字),立即报错:
{ "error": "invalid type: expected number, found string", "field": "nodes[0].width", "value": "120px" }第二步:语义层校验
检查节点 ID 是否唯一、边的from/to是否存在于nodes数组、是否存在循环引用。这里有个关键设计:它不禁止自环边(self-loop),但强制要求radius参数。因为 SVG 渲染自环需要精确的贝塞尔曲线控制点,而随机生成会导致线条扭曲。校验器会计算:
radius = max(node.width, node.height) * 0.3并写入edges[i]的radius字段,确保所有自环弧度一致。
第三步:拓扑层校验
运行 Tarjan 算法检测强连通分量(SCC)。如果发现 SCC 包含超过 3 个节点,触发警告:“检测到复杂循环依赖,请确认是否为设计意图”。这不是错误,而是提示——因为出版级图解要求逻辑清晰,过度循环会降低可读性。我在实际项目中遇到过:某支付系统图因“风控→账务→清算→风控”闭环,被校验器标记,最终拆分为“资金流”和“风控流”两张图,反而提升了评审效率。
实操心得:校验器输出的错误信息直接映射到源 JSON 行号。比如
"line": 42, "column": 15,配合 VS Code 的 JSON 支持,双击即可跳转定位。这比 draw.io 的“红色感叹号图标”高效十倍。
3.2 节点渲染:从抽象类型到视觉符号的精准映射
diagram-design定义了 7 种内置节点类型:service、database、queue、cache、gateway、client、external。每种类型对应一套 SVG 路径模板,而非 CSS class。以database为例:
<defs> <g id="icon-database"> <rect x="-30" y="-20" width="60" height="40" rx="4" fill="#4A90E2"/> <path d="M-25,-15 L-25,15 M-15,-15 L-15,15 M-5,-15 L-5,15 M5,-15 L5,15 M15,-15 L15,15" stroke="#FFFFFF" stroke-width="1.5"/> </g> </defs> <!-- 使用 --> <use href="#icon-database" transform="translate(200,100)"/>这种设计带来三个优势:
- 视觉一致性:所有数据库图标,无论大小,都保持 3:2 宽高比和 4px 圆角,符合 ISO/IEC/IEEE 42010 标准对“数据存储”符号的定义。
- 缩放保真度:
<use>引用<defs>中的图形,缩放时路径顶点坐标自动重算,不会出现 CSSbackground-size导致的像素化。 - 主题可切换:只需替换
<defs>中的fill值,即可批量修改所有数据库颜色。项目提供theme-dark.json和theme-light.json,一键切换整图色调。
更关键的是,它支持类型继承。你可以定义新类型postgres-db,继承database的基础形状,仅覆盖fill和添加小图标:
{ "types": { "postgres-db": { "extends": "database", "fill": "#336791", "icon": "postgres-logo.svg" } } }渲染时,系统先加载database模板,再叠加postgres-logo.svg的<g>元素。这种组合优于 CSS 的:before伪元素,因为 SVG 图标可独立设置opacity和transform,且支持无障碍title。
3.3 边线绘制:贝塞尔曲线算法的手动调优
diagram-design的边线不是简单直线,而是智能贝塞尔曲线。算法核心是:根据起点、终点、中间障碍物位置,动态计算控制点。具体分四步:
Step 1:基础方向判定
计算向量(dx, dy) = (x2-x1, y2-y1),归一化后得到单位方向向量。若|dx| > |dy|,判定为水平主导;否则为垂直主导。
Step 2:控制点初筛
- 水平主导:控制点 y 坐标设为
(y1+y2)/2,x 坐标在x1和x2之间线性插值; - 垂直主导:控制点 x 坐标设为
(x1+x2)/2,y 坐标在y1和y2之间线性插值。
Step 3:障碍物避让
扫描所有节点 bounding box,若控制点落入某节点矩形内,则沿垂直方向偏移 30px。例如,若控制点(150,80)落入auth-svc节点(x:120,y:60,w:140,h:70),则 y 坐标改为80+30=110。
Step 4:曲率优化
引入curvature参数(默认 0.6),公式为:
cp1_x = x1 + (x2-x1) * curvature cp1_y = y1 + (y2-y1) * curvature * 0.5 cp2_x = x2 - (x2-x1) * curvature cp2_y = y2 - (y2-y1) * curvature * 0.5这个公式保证曲线平滑,且两端切线与连线方向一致。我对比过 D3.js 的curveBasis,发现其在节点密集区易产生“蛇形抖动”,而手动调优的贝塞尔曲线始终稳定。
注意:项目禁用
<line>元素画边,因为<line>无法设置stroke-linecap="round"时的端点圆角半径。所有边线均用<path d="M... C...">,确保端点与节点边缘自然融合。
3.4 文字渲染:基线对齐与自动换行的像素级控制
架构图文字常面临两大难题:中英文混排时基线错位、长标签强制换行破坏布局。diagram-design的解决方案是放弃textLength属性,改用<tspan>手动分行。
算法流程:
- 计算单行最大宽度:
maxWidth = node.width * 0.8 - 将标签文本按空格/标点切分为词元(tokens)
- 逐个累加词元宽度(通过
getComputedTextLength()测量),超限时插入<tspan x="0" dy="1.3em"> - 对每一行,单独设置
dominant-baseline="middle"和text-anchor="middle"
关键技巧在于dy="1.3em":em单位基于当前<text>的font-size,1.3是经验值,确保行间距既不拥挤也不松散。我测试过 12px~24px 字号,1.3em始终保持最佳可读性。
更绝的是中英文基线校准。系统检测到字符 Unicode 范围:
- 中文(
\u4e00-\u9fff)、日文(\u3040-\u309f)、韩文(\uac00-\ud7af)→ 使用dominant-baseline="central" - 英文、数字、符号 → 使用
dominant-baseline="middle"
这样,"用户服务 (User Service)"中的中文和英文能严格对齐在同一水平线上,避免传统方案中英文下沉的尴尬。
3.5 分组与标注:语义化容器的 SVG 实现
groups不是视觉分组,而是逻辑容器。每个 group 渲染为<g>元素,并附加>{ "annotations": [ { "type": "note", "position": { "x": 400, "y": 300 }, "content": "此处采用 Redis Cluster 模式,分片数 16", "width": 200, "height": 60 } ] }
渲染时,note类型生成带阴影的 rounded rectangle +<text>,warning类型则用黄色三角形图标 + 红色文字。所有 annotation 都支持z-index排序,确保不会被节点遮挡。
4. 实操全流程:从零开始生成一张可交付的架构图
4.1 环境准备:零依赖起步
你不需要 Node.js、不需要 Python、不需要 Docker。只需要:
- 一台能上网的电脑(用于首次下载)
- 任意文本编辑器(VS Code / Sublime / 记事本均可)
- 现代浏览器(Chrome 90+ / Firefox 85+)
访问https://github.com/diagram-design/diagram-design/releases,下载最新版diagram-design-v1.2.0.zip。解压后得到:
diagram-design/ ├── index.html # 主页面,可直接双击打开 ├── demo.json # 示例数据 ├── themes/ # 主题配置 │ ├── light.json │ └── dark.json └── icons/ # 自定义图标提示:
index.html是完整的单页应用,所有 JS/CSS/SVG 都内联在文件中。你可以把它拷贝到任何目录,甚至 U 盘,双击即用。我曾在客户现场,用公司笔记本(无管理员权限)直接运行,全程离线。
4.2 数据准备:用 JSON 定义你的第一张图
打开demo.json,这是标准模板。我们以“电商订单系统”为例,逐步构建:
Step 1:定义核心节点
{ "nodes": [ { "id": "web-app", "label": "Web 前端", "x": 100, "y": 100, "width": 140, "height": 60, "type": "client" }, { "id": "api-gw", "label": "API 网关", "x": 300, "y": 100, "width": 120, "height": 60, "type": "gateway" }, { "id": "order-svc", "label": "订单服务", "x": 500, "y": 80, "width": 130, "height": 70, "type": "service" }, { "id": "payment-svc", "label": "支付服务", "x": 500, "y": 180, "width": 130, "height": 70, "type": "service" }, { "id": "redis", "label": "Redis 缓存", "x": 700, "y": 130, "width": 110, "height": 60, "type": "cache" }, { "id": "mysql", "label": "MySQL 主库", "x": 900, "y": 130, "width": 130, "height": 70, "type": "database" } ] }Step 2:添加连接关系
"edges": [ { "from": "web-app", "to": "api-gw", "label": "HTTPS" }, { "from": "api-gw", "to": "order-svc", "label": "gRPC" }, { "from": "api-gw", "to": "payment-svc", "label": "gRPC" }, { "from": "order-svc", "to": "redis", "label": "缓存读写" }, { "from": "order-svc", "to": "mysql", "label": "事务写入" }, { "from": "payment-svc", "to": "mysql", "label": "事务写入" } ]Step 3:添加逻辑分组
"groups": [ { "id": "core-services", "label": "核心服务", "nodes": ["order-svc", "payment-svc"], "x": 450, "y": 50, "width": 200, "height": 180, "style": "dashed" } ]Step 4:补充技术注释
"annotations": [ { "type": "note", "position": { "x": 750, "y": 200 }, "content": "Redis Cluster 分片数 16,QPS ≥ 50k", "width": 220, "height": 60 } ]保存为order-system.json。
4.3 渲染与调试:浏览器里的实时工作流
双击index.html,页面加载后点击右上角“Load JSON”,选择order-system.json。图立即渲染。此时开启 Chrome DevTools(F12),切换到 Elements 面板,你会看到:
<svg>根元素,viewBox="0 0 1200 800"- 所有节点都是
<g id="web-app">...</g>形式 - 边线是
<path d="M100,100 C150,100 250,100 300,100"> - 文字是
<text><tspan>Web 前端</tspan></text>
调试技巧:
- 修改 JSON 中某个节点的
x值,保存后刷新页面,观察位置变化——这是最直观的坐标调试法。 - 在 Console 输入
diagram.exportSVG(),返回当前 SVG 字符串,可直接复制到 Inkscape 编辑。 - 右键节点 → “Inspect Element”,查看
>@supports (font-format(woff2)) { @font-face { /* WOFF2 */ } } @supports not (font-format(woff2)) { @font-face { /* WOFF fallback */ } }5.3 边线重叠:多条边汇聚到同一节点时如何区分?
diagram-design默认启用“边线偏移”算法。当检测到to节点有 ≥3 条入边时,自动将边线终点向节点中心偏移 8px,并按角度均匀分布。例如,3 条边分别偏移到0°、120°、240°方向。你可在 JSON 中关闭此功能:"config": { "edgeOffset": false }但强烈建议保留,默认算法已通过 200+ 真实架构图验证。
5.4 主题失效:为什么切换 dark theme 后颜色没变?
主题文件
themes/dark.json必须与index.html同目录,且index.html中的<script>标签需指向正确路径。检查<script src="themes/dark.json"></script>是否存在。更稳妥的做法是:在 JSON 数据中直接指定主题:{ "theme": "dark", "nodes": [ ... ] }5.5 性能瓶颈:渲染 200+ 节点卡顿怎么办?
这不是 bug,而是设计预期。
diagram-design的性能拐点在 150 节点左右。解决方案不是优化,而是重构图结构:- 拆分:将“全系统架构图”拆为“前端架构”、“后端服务”、“数据层”三张图
- 抽象:用
group替代单个节点,例如将 12 个 Kafka Topic 合并为kafka-cluster分组 - 过滤:在 JSON 中添加
"visible": false字段,隐藏非关键节点
我在处理某银行核心系统(420 节点)时,按业务域拆为 7 张图,每张 ≤80 节点,评审效率提升 40%。
6. 进阶玩法:让架构图真正“活”起来
6.1 与代码仓库联动:自动生成架构图
利用 GitHub Actions,在每次
src/architecture/目录变更时,自动运行脚本生成 JSON:# .github/workflows/generate-diagram.yml name: Generate Architecture Diagram on: push: paths: - 'src/architecture/**' jobs: generate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Generate JSON run: | # 从代码注释提取组件信息 grep -r "ARCHITECTURE:" src/ | \ awk -F'ARCHITECTURE:' '{print $2}' | \ jq -nR '[{id: ., label: ., type: "service"}]' > architecture.json - name: Commit Diagram run: | git config --local user.name 'github-actions' git config --local user.email 'actions@github.com' git add architecture.json git commit -m "chore: update architecture diagram"生成的
architecture.json可直接被diagram-design加载。这样,架构图不再是静态文档,而是代码的“活镜像”。6.2 嵌入交互能力:点击跳转源码