PlantUML 输出交互式 SVG:点击跳转、tooltip、JS 联动

puml.online

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

方法 3:%%tooltip(实验)

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
# inject_tooltip.py
import re, sys, pathlib

svg = pathlib.Path(sys.argv[1]).read_text()
metadata = pathlib.Path(sys.argv[2]).read_text()
# metadata 是 JSON: {"us": "User Service handles ...", "os": "Order Service manages ..."}

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
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> 前插入 JS
svg = svg.replace('</svg>', f'{js}</svg>')
pathlib.Path(sys.argv[1]).write_text(svg)

PlantUML 默认输出的 SVG 里节点 element 带 data-node-idid 属性——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', // user-service
'os': 'warn', // order-service
'ps': 'down', // payment-service
};

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 是企业内部页面:

  1. 加载 PlantUML 渲染的 SVG
  2. 注入 post-process JS(点击节点打开 wiki 页面)
  3. 定时从 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 的简洁性。

  • 标题: PlantUML 输出交互式 SVG:点击跳转、tooltip、JS 联动
  • 作者: puml.online
  • 创建于 : 2026-07-30 17:10:00
  • 更新于 : 2026-08-14 21:34:29
  • 链接: https://puml.online/blog/plantuml-interactive-webview/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。