PlantUML 输出交互式 SVG:点击跳转、tooltip、JS 联动
PlantUML 输出的 SVG 默认是「死的」——只能看。但用 map 指令或 post-process 注入 JS,可以让图成为导航地图 :点击节点跳转到文档、悬浮显示 tooltip、调用 API 拉数据。
三种交互层次
层次
实现
适用
静态图
默认 SVG
文档、PPT
点击跳转
[[url]] 或 post-process 注入 <a>
wiki 导航
悬浮 tooltip
post-process JS
架构概览
JS 联动
post-process JS 注入
监控大盘、设计器
方法 1:map 指令 + 点击跳转(PlantUML 原生) 1 2 3 4 5 6 7 8 @startuml component "User Service" as us [[https://wiki.company.com/user-service]] component "Order Service" as os [[https://wiki.company.com/order-service]] component "Payment Service" as ps [[https://wiki.company.com/payment-service]] us --> os os --> ps @enduml
[[url]] 是 PlantUML 原生支持的链接语法——生成出来的 SVG 节点有 <a> 包裹,点击跳转 。
应用场景 :
架构图点击进 wiki
类图点击进源码
流程图点击进操作手册
方法 2:map 指令定义可点击区域 1 2 3 4 5 6 7 @startuml map "Architecture Map" { us => https://wiki/user-service os => https://wiki/order-service ps => https://wiki/payment-service } @enduml
map 指令是 PlantUML 的目录索引图 ——每个 entry 是一个带链接的小卡片。适合放在 README / 文档首页 。
map 的高级语法:
1 2 3 4 5 map "Service Directory" { us => https://wiki/user us => [User Service] => https://wiki/user } @enduml
1 2 3 4 5 @startuml component "User Service" as us %%tooltip "Handles user registration, login, profile" component "Order Service" as os %%tooltip "Manages shopping cart and checkout" us --> os @enduml
%%tooltip 是 PlantUUM 1.2024+ 实验性功能,生成的 SVG 节点带 <title> 元素——鼠标悬浮显示浏览器原生 tooltip 。
注意 :%%tooltip 是 注释级别指令 ,写在元素同一行末尾。不是 !define 也不是 skinparam 。
方法 4:Post-process SVG 注入 JS PlantUML 不支持的内置交互,可以用 Python/Node post-process 注入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import re, sys, pathlibsvg = pathlib.Path(sys.argv[1 ]).read_text() metadata = pathlib.Path(sys.argv[2 ]).read_text() svg = svg.replace( '<svg ' , f'<svg data-tooltips="{pathlib.Path(sys.argv[2 ]).name} " ' ) svg = re.sub( r'data-node-id="(\w+)"' , lambda m: f'data-node-id="{m.group(1 )} " data-tooltip="{json.loads(metadata).get(m.group(1 ), "" )} "' , svg, ) js = """ <script> document.querySelectorAll('[data-tooltip]').forEach(el => { const t = el.getAttribute('data-tooltip'); if (!t) return; el.addEventListener('mouseenter', () => { const tip = document.createElement('div'); tip.className = 'plantuml-tooltip'; tip.textContent = t; document.body.appendChild(tip); const r = el.getBoundingClientRect(); tip.style.left = (r.left + r.width/2) + 'px'; tip.style.top = (r.top - tip.offsetHeight - 5) + 'px'; el._tip = tip; }); el.addEventListener('mouseleave', () => { if (el._tip) el._tip.remove(); }); }); </script> """ svg = svg.replace('</svg>' , f'{js} </svg>' ) pathlib.Path(sys.argv[1 ]).write_text(svg)
PlantUML 默认输出的 SVG 里节点 element 带 data-node-id 或 id 属性 ——post-process 用 querySelector 选目标节点注入事件。
方法 5:实时数据联动 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <!DOCTYPE html > <html > <head > <style > .status-ok { fill : green !important ; } .status-warn { fill : orange !important ; } .status-down { fill : red !important ; } </style > </head > <body > <object data ="architecture.svg" type ="image/svg+xml" id ="diagram" > </object > <script > const statuses = { 'us' : 'ok' , 'os' : 'warn' , 'ps' : 'down' , }; document .getElementById ('diagram' ).addEventListener ('load' , () => { const svgDoc = document .getElementById ('diagram' ).contentDocument ; for (const [nodeId, status] of Object .entries (statuses)) { const node = svgDoc.querySelector (`[data-node-id="${nodeId} "]` ); if (node) node.classList .add (`status-${status} ` ); } }); setInterval (async () => { const r = await fetch ('/api/service-status' ); const data = await r.json (); const svgDoc = document .getElementById ('diagram' ).contentDocument ; for (const [id, status] of Object .entries (data)) { const node = svgDoc.querySelector (`[data-node-id="${id} "]` ); if (node) { node.classList .remove ('status-ok' , 'status-warn' , 'status-down' ); node.classList .add (`status-${status} ` ); } } }, 10000 ); </script > </body > </html >
架构图实时显示服务健康状态 ——绿/橙/红三色监控盘。
<object> 嵌入 SVG,JavaScript 通过 contentDocument 访问 SVG DOM ——跨域需要 SVG 服务端发 Access-Control-Allow-Origin。
方法 6:节点点击触发详情面板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <!DOCTYPE html > <html > <head > <style > .detail-panel { position : fixed; right : 0 ; top : 0 ; width : 300px ; height : 100vh ; background : white; box-shadow : -2px 0 8px rgba (0 ,0 ,0 ,0.1 ); padding : 20px ; display : none; } .detail-panel .open { display : block; } </style > </head > <body > <object data ="architecture.svg" type ="image/svg+xml" id ="diagram" > </object > <div class ="detail-panel" id ="panel" > </div > <script > document .getElementById ('diagram' ).addEventListener ('load' , () => { const svgDoc = document .getElementById ('diagram' ).contentDocument ; svgDoc.querySelectorAll ('[data-node-id]' ).forEach (node => { node.style .cursor = 'pointer' ; node.addEventListener ('click' , async () => { const id = node.getAttribute ('data-node-id' ); const r = await fetch (`/api/services/${id} /details` ); const detail = await r.json (); document .getElementById ('panel' ).innerHTML = ` <h2>${detail.name} </h2> <p>${detail.description} </p> <ul> <li>Owner: ${detail.owner} </li> <li>Repo: ${detail.repo} </li> <li>On-call: ${detail.oncall} </li> </ul> ` ; document .getElementById ('panel' ).classList .add ('open' ); }); }); }); </script > </body > </html >
点击图里某个 service,右侧滑出详情面板 ——架构图成为团队 wiki 的导航入口。
实战:用 PlantUML 制作 CI 流水线可视化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @startuml component "Git Push" as git component "Lint" as lint component "Test" as test component "Build" as build component "Deploy Staging" as staging component "Deploy Prod" as prod git --> lint lint --> test : "fail" test --> build : "pass" build --> staging staging --> prod : "manual approve" @enduml
CI 流水线可视化,每个节点对应一个 CI 阶段。前端 post-process 把节点颜色改成对应阶段状态 (running=蓝,passed=绿,failed=红,pending=灰)。
实战:架构图嵌入 Confluence + 实时状态 1 2 3 4 5 6 <ac:structured-macro ac:name ="html" > <ac:plain-text-body > <![CDATA[ <iframe src="https://wiki.internal/diagram-viewer?svg=architecture.svg" width="100%" height="600"></iframe> ]]></ac:plain-text-body > </ac:structured-macro >
diagram-viewer 是企业内部页面:
加载 PlantUML 渲染的 SVG
注入 post-process JS(点击节点打开 wiki 页面)
定时从 Prometheus API 拉服务状态,更新节点颜色
安全考虑 SVG 内嵌 JS 有 XSS 风险 :
PlantUML 生成的 SVG 默认不 包含 <script>——安全
但 post-process 注入 JS 时,要把 SVG 部署到可信域 (企业内部 CDN)
不要 让用户上传 .puml 然后渲染 SVG 给别人看——攻击者可以注入恶意 JS
生产做法 :
SVG 上传到 cdn.internal.company.com
JS post-process 在 CI 阶段生成,部署到内部 wiki
用 DOMPurify 净化 SVG
决策树 1 2 3 4 5 6 需要交互吗? ├─ 不需要 → 默认 SVG,完事 ├─ 点击跳转 wiki → [[url]] 语法 ├─ 悬浮 tooltip → %%tooltip(简单)/ post-process(复杂) ├─ 实时状态显示 → post-process JS + API └─ 复杂交互(详情面板/编辑) → Vue/React 包装 SVG
最简方案 用 [[url]]——零 JS。最复杂方案 用 React/Vue 把 SVG 当 SVG-as-component,完全编程控制——但失去了 PlantUML 的简洁性。