最近在开发一个漫剧系统时,我遇到了一个有趣的挑战:如何让非技术背景的编辑人员也能轻松构建复杂的剧情流程?传统的代码式配置显然行不通,而拖拽式的工作流编辑器就成了最佳选择。在对比了几个可视化库后,我选择了React Flow——一个基于React的开源库,专门用来快速创建和管理流程图、节点图。
想象一下,你要搭建一个漫剧的剧情线:角色出场、对话分支、场景切换、特效触发……这些原本需要写代码的逻辑,现在只需要像搭积木一样拖拽连接就能完成。这就是React Flow带给我的第一感受。
React Flow本质上是一个React组件库,它帮你用代码画出“节点”和“连线”,构成流程图或图形界面。它支持节点拖拽、连线、缩放和平移,操作起来流畅自然,就像在使用专业的设计软件。
在开始动手前,先了解几个关键术语:
让我们从一个最简单的例子开始。在React项目中安装React Flow(现在也叫@xyflow/react):
npm install @xyflow/react
然后创建一个基本的流程图组件:
import React from 'react';
import { ReactFlow, Background } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
export default function App() {
const nodes = [
{
id: '1',
position: { x: 100, y: 100 },
data: { label: '开场场景' }
},
{
id: '2',
position: { x: 300, y: 100 },
data: { label: '主角登场' }
}
];
const edges = [
{
id: 'e1-2',
source: '1',
target: '2',
}
];
return (
<div style={{width: '100vw', height: '100vh'}}>
<ReactFlow nodes={nodes} edges={edges}>
<Background />
</ReactFlow>
</div>
)
}
```ts
import React from 'react';
import { ReactFlow, Background } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
export default function App() {
const nodes = [
{
id: '1',
position: { x: 100, y: 100 },
data: { label: '开场场景' }
},
{
id: '2',
position: { x: 300, y: 100 },
data: { label: '主角登场' }
}
];
const edges = [
{
id: 'e1-2',
source: '1',
target: '2',
}
];
return (
<div style={{width: '100vw', height: '100vh'}}>
<ReactFlow nodes={nodes} edges={edges}>
<Background />
</ReactFlow>
</div>
)
}
这是提升用户体验的关键——用户双击节点就能修改剧情描述:
const onNodeDoubleClick = (_, node) => {
const newLabel = prompt('请输入新的节点标签', node.data.label);
if (newLabel) {
setNodes((nds) =>
nds.map((n) =>
n.id === node.id ? { ...n, data: { ...n.data, label: newLabel } } : n
)
);
}
};
```ts
const onNodeDoubleClick = (_, node) => {
const newLabel = prompt('请输入新的节点标签', node.data.label);
if (newLabel) {
setNodes((nds) =>
nds.map((n) =>
n.id === node.id ? { ...n, data: { ...n.data, label: newLabel } } : n
)
);
}
};
React Flow提供了丰富的事件回调,让交互变得简单:
import { applyNodeChanges, applyEdgeChanges } from '@xyflow/react';
function Flow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[]
);
const onEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[]
);
return (
<ReactFlow
nodes={nodes}
onNodesChange={onNodesChange}
edges={edges}
onEdgesChange={onEdgesChange}
fitView
>
<Background />
<Controls />
</ReactFlow>
);
}
```ts
import { applyNodeChanges, applyEdgeChanges } from '@xyflow/react';
function Flow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[]
);
const onEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[]
);
return (
<ReactFlow
nodes={nodes}
onNodesChange={onNodesChange}
edges={edges}
onEdgesChange={onEdgesChange}
fitView
>
<Background />
<Controls />
</ReactFlow>
);
}
这样,当用户拖拽节点或调整连线时,状态会自动更新。
默认的矩形节点太单调了?在漫剧系统中,不同的剧情元素应该有不同外观。React Flow支持完全自定义节点组件:
import { Handle, Position } from '@xyflow/react';
const SceneNode = ({ data }) => {
return (
<div style={{
padding: '15px',
border: '2px solid #4CAF50',
borderRadius: '8px',
background: '#E8F5E9',
minWidth: '120px'
}}>
<Handle type="target" position={Position.Top} />
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '5px' }}>
场景节点
</div>
<strong>{data.label}</strong>
</div>
<Handle type="source" position={Position.Bottom} />
</div>
);
};
const DialogueNode = ({ data }) => {
return (
<div style={{
padding: '15px',
border: '2px solid #2196F3',
borderRadius: '50%',
background: '#E3F2FD',
width: '100px',
height: '100px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<Handle type="target" position={Position.Top} />
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '12px', color: '#666' }}>
对话
</div>
<strong>{data.label}</strong>
</div>
<Handle type="source" position={Position.Bottom} />
</div>
);
};
```ts
import { Handle, Position } from '@xyflow/react';
const SceneNode = ({ data }) => {
return (
<div style={{
padding: '15px',
border: '2px solid #4CAF50',
borderRadius: '8px',
background: '#E8F5E9',
minWidth: '120px'
}}>
<Handle type="target" position={Position.Top} />
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '5px' }}>
场景节点
</div>
<strong>{data.label}</strong>
</div>
<Handle type="source" position={Position.Bottom} />
</div>
);
};
const DialogueNode = ({ data }) => {
return (
<div style={{
padding: '15px',
border: '2px solid #2196F3',
borderRadius: '50%',
background: '#E3F2FD',
width: '100px',
height: '100px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<Handle type="target" position={Position.Top} />
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '12px', color: '#666' }}>
对话
</div>
<strong>{data.label}</strong>
</div>
<Handle type="source" position={Position.Bottom} />
</div>
);
};
然后在ReactFlow组件中注册这些自定义节点类型:
const nodeTypes = {
scene: SceneNode,
dialogue: DialogueNode,
// 可以添加更多类型...
};
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
>
{/* ... */}
</ReactFlow>
```ts
const nodeTypes = {
scene: SceneNode,
dialogue: DialogueNode,
// 可以添加更多类型...
};
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
>
{/* ... */}
</ReactFlow>
现在,你的节点数组可以指定类型了:
const nodes = [
{
id: '1',
type: 'scene', // 使用自定义场景节点
data: { label: '森林入口' },
position: { x: 100, y: 100 },
},
{
id: '2',
type: 'dialogue', // 使用自定义对话节点
data: { label: '“你是谁?”' },
position: { x: 300, y: 100 },
},
];
```ts
const nodes = [
{
id: '1',
type: 'scene', // 使用自定义场景节点
data: { label: '森林入口' },
position: { x: 100, y: 100 },
},
{
id: '2',
type: 'dialogue', // 使用自定义对话节点
data: { label: '“你是谁?”' },
position: { x: 300, y: 100 },
},
];
React Flow提供了几个开箱即用的辅助组件,大大提升了用户体验:
import { ReactFlow, MiniMap, Controls, Background } from '@xyflow/react';
<ReactFlow nodes={nodes} edges={edges}>
<MiniMap /> {/* 小地图,快速导航 */}
<Controls /> {/* 缩放、平移控件 */}
<Background /> {/* 背景网格,帮助对齐 */}
</ReactFlow>
```ts
import { ReactFlow, MiniMap, Controls, Background } from '@xyflow/react';
<ReactFlow nodes={nodes} edges={edges}>
<MiniMap /> {/* 小地图,快速导航 */}
<Controls /> {/* 缩放、平移控件 */}
<Background /> {/* 背景网格,帮助对齐 */}
</ReactFlow>
当节点很多时,手动排列会很麻烦。这时可以结合Dagre等布局算法自动排列节点。Dagre是一种层次布局算法,特别适合有向无环图(DAG)。
在漫剧系统中,剧情通常有明确的先后顺序(有向),且不会形成循环(无环),这正是Dagre的用武之地。它能自动计算节点的位置,让整个流程图看起来更整齐、专业。
当剧情线非常复杂时,节点数量可能达到几百个。React Flow默认只渲染视口内的节点,这已经做了很好的优化。但还可以进一步:
在漫剧系统中,不是所有节点都能随意连接。比如:
可以通过自定义连接逻辑来实现验证:
const isValidConnection = (connection) => {
const sourceNode = nodes.find(n => n.id === connection.source);
const targetNode = nodes.find(n => n.id === connection.target);
// 示例规则:结束节点不能作为源节点
if (sourceNode.type === 'end') {
return false;
}
// 示例规则:开始节点不能作为目标节点
if (targetNode.type === 'start') {
return false;
}
return true;
};
<ReactFlow
nodes={nodes}
edges={edges}
onConnect={(params) => {
if (isValidConnection(params)) {
setEdges((eds) => addEdge(params, eds));
} else {
alert('不允许的连接!');
}
}}
/>
```ts
const isValidConnection = (connection) => {
const sourceNode = nodes.find(n => n.id === connection.source);
const targetNode = nodes.find(n => n.id === connection.target);
// 示例规则:结束节点不能作为源节点
if (sourceNode.type === 'end') {
return false;
}
// 示例规则:开始节点不能作为目标节点
if (targetNode.type === 'start') {
return false;
}
return true;
};
<ReactFlow
nodes={nodes}
edges={edges}
onConnect={(params) => {
if (isValidConnection(params)) {
setEdges((eds) => addEdge(params, eds));
} else {
alert('不允许的连接!');
}
}}
/>
用户辛辛苦苦搭建的剧情线当然要能保存。我使用了Supabase(也可以换成任何后端):
const saveFlow = async (flowName) => {
const flowData = {
name: flowName,
nodes,
edges,
createdAt: new Date().toISOString()
};
// 保存到数据库
const { error } = await supabase
.from('flows')
.insert(flowData);
if (error) {
console.error('保存失败:', error);
alert('保存失败,请重试');
} else {
alert('剧情线保存成功!');
}
};
const loadFlow = async (flowId) => {
const { data, error } = await supabase
.from('flows')
.select('*')
.eq('id', flowId)
.single();
if (error) {
console.error('加载失败:', error);
} else {
setNodes(data.nodes);
setEdges(data.edges);
}
};
```ts
const saveFlow = async (flowName) => {
const flowData = {
name: flowName,
nodes,
edges,
createdAt: new Date().toISOString()
};
// 保存到数据库
const { error } = await supabase
.from('flows')
.insert(flowData);
if (error) {
console.error('保存失败:', error);
alert('保存失败,请重试');
} else {
alert('剧情线保存成功!');
}
};
const loadFlow = async (flowId) => {
const { data, error } = await supabase
.from('flows')
.select('*')
.eq('id', flowId)
.single();
if (error) {
console.error('加载失败:', error);
} else {
setNodes(data.nodes);
setEdges(data.edges);
}
};
在漫剧系统的开发过程中,我发现React Flow的潜力远不止于此:
复杂的漫剧可能有嵌套结构:一个大场景包含多个子场景。React Flow支持分组节点(Group Node),可以将多个节点组织在一起。
通过自定义节点和连接逻辑,可以实现:
结合WebSocket,可以实现多人在线编辑同一个剧情线,就像在线文档协作一样。
保存剧情线的历史版本,允许用户回滚到之前的某个状态,这对创作类应用特别有用。
经过在漫剧系统中的实际应用,我总结了React Flow的几个核心优势:
最重要的是,它让技术变得透明。我的编辑同事不需要知道React、不需要懂状态管理,他们只需要拖拽、连接、输入文字,就能构建出复杂的漫剧剧情线。这大概就是技术最有价值的时刻:让复杂变得简单,让专业变得平民。
如果你也在考虑为你的应用添加可视化工作流功能,React Flow绝对值得一试。它就像一套高质量的乐高积木,给你提供了所有基础零件,而你可以用它们搭建出任何想象中的世界。
在漫剧系统的开发路上,React Flow不仅是一个工具,更是连接技术实现与用户创意的桥梁。当看到非技术同事也能轻松搭建出复杂的剧情流程图时,那种成就感,大概就是做技术的最大快乐吧。
本文基于React Flow的实际开发经验总结,希望能给正在探索可视化工作流的你一些启发。如果你有更多有趣的应用场景或问题,欢迎在评论区交流!