PlantUML 与交互式图:react-flow / Vue Flow / vis.js 当 PlantUML 不够用时

puml.online

PlantUML 出 SVG / PNG / PDF,没有交互——不能拖、不能高亮、不能动态改。这是「文档」而不是「工具」——很多前端项目想要后者。这一篇整理 PlantUML 与主流交互式图库的搭配实战。

PlantUML 的定位

PlantUML 输出 静态

  • ✅ SVG(嵌入文档)
  • ✅ PNG(截图)
  • ✅ PDF(打印)
  • 不能拖动节点
  • 不能点击高亮
  • 不能实时编辑改图

如果你需要「拖拽画图」「图节点交互点击展开」等能力——PlantUML 不够用。需要前端图库。

当下主流的交互式图库

维护方 关系 复杂度
react-flow xyflow/React 节点 + 边 + handle 低 → 中
Vue Flow @vue-flow 同上 (Vue 版) 低 → 中
vis.js visjs.org 时间线 / 网络图
Cytoscape.js cytoscape.org 复杂 network / 关系图 中 → 高
D3 D3 Observable 数据驱动 SVG

react-flow(最常用)

安装

1
2
npm install reactflow
# 不要用 react-flow 包(新版本叫 reactflow)

最小 demo

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
import ReactFlow, {
Background,
Controls,
MiniMap
} from 'reactflow';
import 'reactflow/dist/style.css';

const nodes = [
{ id: '1', position: { x: 100, y: 100 }, data: { label: 'Frontend' } },
{ id: '2', position: { x: 300, y: 200 }, data: { label: 'Backend' } }
];

const edges = [
{ id: 'e1-2', source: '1', target: '2' }
];

function MyDiagram() {
return (
<div style={{ width: '100%', height: '600px' }}>
<ReactFlow nodes={nodes} edges={edges} fitView>
<Background />
<Controls />
<MiniMap />
</ReactFlow>
</div>
);
}

优势

  • React-native API——天然融入 React 生态
  • 节点可拖、可编辑、可高亮
  • handle 系统——节点之间能控制连线端点
  • 迷你地图+Controls内置
  • 动画路径——边能动画

劣势

  • 状态全在 React state——大型图(>100 节点)重渲染慢
  • 节点/边类型得自己定义或选社区预设
  • 复杂 layout(如 ELK)靠外部算法

Vue Flow(react-flow 的 Vue 版本)

安装

1
npm install @vue-flow/core

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup>
import { VueFlow } from '@vue-flow/core'
import '@vue-flow/core/dist/style.css'

const nodes = [
{ id: '1', position: { x: 100, y: 100 }, data: { label: 'Frontend' } }
]
const edges = [
{ id: 'e1-2', source: '1', target: '2' }
]
</script>

<template>
<div style="width: 100%; height: 600px">
<VueFlow :nodes="nodes" :edges="edges" />
</div>
</template>

Vue Flow API 几乎和 react-flow 同名同行为,只是把 React 的 <JSX/> 换成 Vue <template/>。我们这里不深入 Vue Flow 后端。

vis.js(时间线 / 网络)

安装

1
npm install vis-network vis-timeline vis-data

时序图(或 PlantUML sequence 替代)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Timeline, DataSet } from 'vis-timeline/standalone';

const items = new DataSet([
{ id: 1, content: '设计', start: '2026-01-01' },
{ id: 2, content: '开发', start: '2026-02-01' },
{ id: 3, content: '上线', start: '2026-04-01' }
]);

const options = {
stack: true,
orientation: { axis: 'top', item: 'top' }
};

new Timeline(container, items, options);

网络图(替换 PlantUML component)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Network } from 'vis-network/standalone';

const nodes = [
{ id: 1, label: 'Frontend' },
{ id: 2, label: 'Backend' }
];

const edges = [
{ from: 1, to: 2, arrows: 'to' }
];

const data = { nodes: new DataSet(nodes), edges: new DataSet(edges) };

new Network(container, data, {});

优势

  • 时间线场景无敌——PlantUML 没有时间线
  • 网络图简单直接——不写一行 state
  • 可画大图(>500 节点)

劣势

  • 看起来「老派」(2014 年项目)
  • React / Vue 包不像 react-flow 那么 native
  • 文档散乱

Cytoscape.js(关系图谱 / 复杂图)

适合什么

  • 知识图谱(KG)
  • 生物信息学网络
  • 大图分析(>1000 节点)
  • 需要复杂选区算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import cytoscape from 'cytoscape';

const cy = cytoscape({
container: document.getElementById('cy'),
elements: [
{ data: { id: 'a', label: 'Node A' } },
{ data: { id: 'b', label: 'Node B' } },
{ data: { id: 'ab', source: 'a', target: 'b' } }
],
style: [
{ selector: 'node', style: { 'background-color': '#666', label: 'data(label)' } },
{ selector: 'edge', style: { 'width': 3, 'line-color': '#ccc' } }
],
layout: { name: 'cose' }
});

优势

  • 图算法完整——cose / fcose / circle / breadthfirst / grid 等
  • selectors——CSS 风格选择节点/边
  • 事件系统——tap / mouseover etc.
  • selectors 联动——实时改样式

劣势

  • 学习曲线陡
  • 不是「React-first」——要包一层 wrapper

D3(最自由)

何时选 D3

  • 需要完全控制渲染
  • 想要非传统图(如弧弦图 / 桑基图 / 力导向)
  • 已有 D3 全家桶(d3-scale、d3-geo 等等)

何时不选

  • 单纯画节点+边——react-flow 更轻
  • 团队不愿写「数据 → SVG」逻辑——cytoscape/vis.js 更快

实战:PlantUML → react-flow 转换

我们项目(puml.online)自己有 PlantUML 编辑器,当我们想做一个「点击节点高亮对应图区域」功能时,需要把 PlantUML 转 react-flow 数据。

解析 PlantUML

PlantUML 输出 SVG——用 jsdom 解析 SVG,从中抽取节点 / 边:

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
import { JSDOM } from 'jsdom';

async function pumlSvgToFlow(svgText) {
const dom = new JSDOM(svgText);
const doc = dom.window.document;

const nodes = [];
const edges = [];

// 找 PlantUML 节点(class="node" 或 polygon、ellipse)
doc.querySelectorAll('.node, g.node, polygon, ellipse').forEach(el => {
const id = el.id; // PlantUML 给每个节点唯一 ID
const transform = el.getAttribute('transform') || '';
const m = transform.match(/translate\(([\d.-]+),([\d.-]+)\)/);
const position = m ? { x: +m[1], y: +m[2] } : { x: 0, y: 0 };
const label = el.querySelector('text')?.textContent || id;

nodes.push({
id,
type: 'default',
position,
data: { label }
});
});

// 找边(path 元素带 stroke)
doc.querySelectorAll('path').forEach(el => {
const d = el.getAttribute('d');
if (d && (d.includes('L') || d.includes('C'))) {
// PlantUML 用 SM 卡来端对节点:起点对应 source,止点对应 target
// 实际需要根据 stroke 模式匹配,但简化为:相邻节点之间连线
edges.push({
id: el.id || crypto.randomUUID(),
source: 'unknown', // 需要从 PlantUML metadata 拿
target: 'unknown'
});
}
});

return { nodes, edges };
}

实际 PlantUML SVG 不直接挂 source/target 信息——你需要 PlantUUM pid 输出(-pipe -tlint-checkonly 输出 AST)来做反向映射。

更好的方法:PlantUML server 返回 JSON AST

1
2
curl -X POST "http://localhost:8080/json" \
--data-urlencode "diagram=@startuml ... @enduml"

返回:

1
2
3
4
5
6
7
8
9
{
"diagram_type": "sequence",
"participants": [
{ "name": "Alice", "type": "actor" }
],
"messages": [
{ "from": "Alice", "to": "Bob", "label": "hello" }
]
}

缺点:官方 plantuml-server 的 /json 路由不稳定,很多版本没实现。需要 check 你的版本。

替代方案:手写转换器

最稳的做法是自己写一个 PlantUML 文本 → react-flow 数据的转换器:

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
47
import { Edge, Node } from 'reactflow';

export interface ParsedDiagram {
type: 'sequence' | 'class' | 'state' | 'component' | 'deployment';
nodes: Node[];
edges: Edge[];
}

class PUMLParser {
parse(code: string): ParsedDiagram {
if (code.includes('@startuml')) {
return this.parseClass(code);
}
throw new Error('Unsupported type');
}

private parseClass(code: string): ParsedDiagram {
const lines = code.split('\n');
const nodes: Node[] = [];
const edges: Edge[] = [];

lines.forEach((line, i) => {
// class Foo {...}
const classMatch = line.match(/^class\s+(\w+)\s*\{/);
if (classMatch) {
nodes.push({
id: classMatch[1],
position: { x: 0, y: i * 100 }, // 简单排版
data: { label: classMatch[1] }
});
}

// relation: A --> B / A --|> B / A *-- B
const relMatch = line.match(/(\w+)\s+(--\|>|--\*|-->|o--|--)\s+(\w+)/);
if (relMatch) {
const [, src, , dst] = relMatch;
edges.push({
id: `${src}-${dst}-${i}`,
source: src,
target: dst
});
}
});

return { type: 'class', nodes, edges };
}
}

通用、不依赖 PlantUML 服务端。复杂语法(嵌套 package / generics)覆盖率 60%——不够时手动 edit。

实战:交互式 PlantUML 编辑器 demo

这是「边写 puml 边看 react-flow」工作流:

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
import { useState, useEffect } from 'react';
import ReactFlow from 'reactflow';
import { PUMLParser } from './puml-parser';

function InteractiveDiagram() {
const [code, setCode] = useState('@startuml\nclass A\nclass B\nA --> B\n@enduml');
const [diagram, setDiagram] = useState(new PUMLParser().parse(code));

useEffect(() => {
setDiagram(new PUMLParser().parse(code));
}, [code]);

return (
<div style={{ display: 'flex', height: '100vh' }}>
<textarea
value={code}
onChange={e => setCode(e.target.value)}
style={{ width: '40%', fontFamily: 'monospace' }}
/>
<div style={{ flex: 1 }}>
<ReactFlow
nodes={diagram.nodes}
edges={diagram.edges}
fitView
nodesDraggable
onNodeClick={(_, node) => alert(`Clicked: ${node.data.label}`)}
/>
</div>
</div>
);
}

为什么这个 demo 有用

  • 编辑 puml 文本 → react-flow 自动重渲染
  • 节点可拖(你可以「手动调布局」
  • 点击节点触发业务逻辑

决策矩阵

场景 推荐
「画图给读者看」(博客 / 文档) PlantUML → SVG
「图能拖、能改、能点击」 react-flow / Vue Flow
时间线 / Gantt vis-timeline
复杂 KG (>1000 节点) Cytoscape.js
桑基图 / 弧弦图 / 等非标准图 D3
「文档 + 「少些交互」 PlantUML + post-processing

PlantUML + react-flow 双栈实战

「文档需要 PlantUML 静态图,工具需要 react-flow 交互图」——一内容两套发布。

1
2
3
4
5
6
7
project/
├── docs/diagrams/ (puml source)
│ ├── auth.puml
│ └── order.puml
└── app/diagrams/ (react-flow data)
├── auth.json (parsed)
└── order.json

scripts/parse-puml.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import fs from 'node:fs';
import path from 'node:path';
import { PUMLParser } from '../parser/puml';

const dir = 'docs/diagrams';
const outDir = 'app/diagrams';
fs.mkdirSync(outDir, { recursive: true });

for (const file of fs.readdirSync(dir)) {
if (!file.endsWith('.puml')) continue;
const code = fs.readFileSync(path.join(dir, file), 'utf-8');
const diagram = new PUMLParser().parse(code);
fs.writeFileSync(
path.join(outDir, file.replace('.puml', '.json')),
JSON.stringify(diagram, null, 2)
);
}

CI 跑:

1
2
- name: Parse PUML  React Flow
run: node scripts/parse-puml.ts

总结:选择时机

  • 只画静态图(文档、博客、PPT) —— PlantUML
  • 能拖能改的画布 —— react-flow / Vue Flow
  • 时间线 / Gantt —— vis-timeline
  • 大图算法分析(1000+ 节点) —— Cytoscape.js
  • 非标准图(sankey 等) —— D3
  • 两者都需要 —— PlantUML 当 source of truth + 解析得到前端图数据

小结

  • PlantUML 是静态 DSL——不负责交互
  • react-flow / Vue Flow 是「拖动」和「点击」的「表」
  • PlantUML SVG → react-flow 转换不容易。靠 PlantUUM server 返回的 JSON AST(不稳定)或自己写 parser
  • 现实选择:「文档」用 PlantUML、「工具」用 react-flow、两者用同一个 source 同步

下一步

  • 标题: PlantUML 与交互式图:react-flow / Vue Flow / vis.js 当 PlantUML 不够用时
  • 作者: puml.online
  • 创建于 : 2026-07-30 13:00:00
  • 更新于 : 2026-08-14 21:34:29
  • 链接: https://puml.online/blog/plantuml-react-flow-integration/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。