第八篇:可视化流程编辑器(上):React Flow核心概念与集成
·
📚 QuantumFlow工作流自动化从入门到精通 - 第8篇
前端架构搭建完成后,我们将进入最激动人心的部分——可视化流程编辑器。本文将深入探讨React Flow库的核心概念,并实现一个功能完整的工作流可视化编辑器。
📋 本文概览
学习目标:
- 理解React Flow的核心架构和设计理念
- 掌握自定义节点组件的开发方法
- 学会创建自定义边组件和连接规则
- 实现完整的拖拽交互功能
- 掌握画布缩放、平移等用户体验优化
- 实现节点连接的业务逻辑验证
技术栈:
- React Flow 11.10+
- React 18+
- TypeScript 5.0+
- TailwindCSS 3.4+
- Zustand 4.0+
预计阅读时间: 35分钟
前置知识: React基础、TypeScript基础、第7篇前端架构知识
🎯 一、为什么选择React Flow?
1.1 可视化编辑器技术选型对比
/**
* 可视化编辑器库对比分析
*/
const visualEditorComparison = {
"React Flow": {
stars: "21.5k+",
bundle_size: "~150KB",
learning_curve: "中等",
pros: [
"React原生,无缝集成",
"高度可定制化",
"性能优秀(虚拟化渲染)",
"活跃的社区支持",
"完善的TypeScript支持",
"内置缩放、平移、选择等交互"
],
cons: [
"需要一定学习成本",
"复杂布局需要额外处理"
],
use_cases: [
"工作流编辑器",
"数据流图",
"思维导图",
"状态机可视化"
],
verdict: "✅ 最佳选择"
},
"D3.js": {
stars: "107k+",
bundle_size: "~250KB",
learning_curve: "陡峭",
pros: [
"功能极其强大",
"可实现任何可视化效果",
"成熟稳定"
],
cons: [
"学习曲线陡峭",
"与React集成复杂",
"开发效率低",
"需要手动处理所有交互"
],
verdict: "⚠️ 过于底层,不推荐"
},
"GoJS": {
stars: "商业库",
bundle_size: "~800KB",
learning_curve: "中等",
pros: [
"功能完整",
"文档详细",
"企业级支持"
],
cons: [
"商业授权($$$)",
"包体积大",
"定制化受限"
],
verdict: "❌ 成本高,不适合开源项目"
},
"Rete.js": {
stars: "9.5k+",
bundle_size: "~100KB",
learning_curve: "中等",
pros: [
"专为节点编辑器设计",
"插件系统",
"轻量级"
],
cons: [
"社区较小",
"React集成不够原生",
"文档不够完善"
],
verdict: "⚠️ 可考虑,但不如React Flow"
}
};
1.2 React Flow核心特性
/**
* React Flow核心特性展示
*/
interface ReactFlowFeatures {
// 核心功能
core: {
node_rendering: "虚拟化渲染,支持数千节点";
edge_rendering: "SVG路径,支持自定义样式";
interaction: "拖拽、缩放、平移、框选";
layout: "自动布局(需配合dagre等库)";
};
// 自定义能力
customization: {
custom_nodes: "完全自定义节点组件";
custom_edges: "自定义边样式和交互";
custom_handles: "自定义连接点位置和样式";
theming: "支持主题定制";
};
// 高级功能
advanced: {
minimap: "小地图导航";
controls: "缩放控制器";
background: "网格背景";
selection: "多选、框选";
undo_redo: "撤销/重做(需自行实现)";
};
// 性能优化
performance: {
virtualization: "视口外节点不渲染";
lazy_loading: "按需加载";
memoization: "智能缓存";
};
}
🏗️ 二、React Flow基础集成
2.1 安装与基础配置
# 安装React Flow
npm install reactflow
# 安装类型定义(已包含在主包中)
# npm install @types/reactflow # 不需要单独安装
2.2 最简单的React Flow示例
// src/features/workflows/components/WorkflowEditor.tsx
import { useCallback } from 'react';
import ReactFlow, {
Node,
Edge,
Controls,
Background,
BackgroundVariant,
useNodesState,
useEdgesState,
addEdge,
Connection,
ConnectionMode,
} from 'reactflow';
import 'reactflow/dist/style.css';
// 初始节点数据
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: '开始' },
position: { x: 250, y: 5 },
},
{
id: '2',
data: { label: 'HTTP请求' },
position: { x: 100, y: 100 },
},
{
id: '3',
data: { label: '数据转换' },
position: { x: 400, y: 100 },
},
{
id: '4',
type: 'output',
data: { label: '结束' },
position: { x: 250, y: 200 },
},
];
// 初始边数据
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e2-4', source: '2', target: '4' },
{ id: 'e3-4', source: '3', target: '4' },
];
export default function WorkflowEditor() {
// 使用React Flow提供的Hooks管理节点和边
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
// 连接节点时的回调
const onConnect = useCallback(
(params: Connection) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
);
return (
<div className="w-full h-screen">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
connectionMode={ConnectionMode.Loose}
fitView
>
{/* 控制器(缩放按钮) */}
<Controls />
{/* 背景网格 */}
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
</ReactFlow>
</div>
);
}
2.3 核心概念解析
/**
* React Flow核心概念
*/
// 1. Node(节点)
interface Node {
id: string; // 唯一标识
type?: string; // 节点类型(用于自定义节点)
data: any; // 节点数据(传递给节点组件)
position: { x: number; y: number }; // 节点位置
// 可选属性
style?: React.CSSProperties; // 节点样式
className?: string; // CSS类名
targetPosition?: Position; // 输入连接点位置
sourcePosition?: Position; // 输出连接点位置
hidden?: boolean; // 是否隐藏
selected?: boolean; // 是否选中
dragging?: boolean; // 是否正在拖拽
draggable?: boolean; // 是否可拖拽
selectable?: boolean; // 是否可选中
connectable?: boolean; // 是否可连接
deletable?: boolean; // 是否可删除
dragHandle?: string; // 拖拽手柄选择器
width?: number; // 节点宽度
height?: number; // 节点高度
parentNode?: string; // 父节点ID(用于分组)
zIndex?: number; // 层级
extent?: 'parent' | [number, number, number, number]; // 拖拽范围限制
}
// 2. Edge(边)
interface Edge {
id: string; // 唯一标识
source: string; // 源节点ID
target: string; // 目标节点ID
// 可选属性
type?: string; // 边类型
sourceHandle?: string; // 源连接点ID
targetHandle?: string; // 目标连接点ID
label?: string | React.ReactNode; // 边标签
labelStyle?: React.CSSProperties; // 标签样式
labelShowBg?: boolean; // 标签是否显示背景
labelBgStyle?: React.CSSProperties; // 标签背景样式
labelBgPadding?: [number, number]; // 标签背景内边距
labelBgBorderRadius?: number; // 标签背景圆角
style?: React.CSSProperties; // 边样式
animated?: boolean; // 是否动画
hidden?: boolean; // 是否隐藏
selected?: boolean; // 是否选中
markerStart?: string | EdgeMarker; // 起点标记
markerEnd?: string | EdgeMarker; // 终点标记
zIndex?: number; // 层级
interactionWidth?: number; // 交互区域宽度
}
// 3. Connection(连接)
interface Connection {
source: string | null; // 源节点ID
target: string | null; // 目标节点ID
sourceHandle?: string | null; // 源连接点ID
targetHandle?: string | null; // 目标连接点ID
}
// 4. Position(位置)
enum Position {
Left = 'left',
Top = 'top',
Right = 'right',
Bottom = 'bottom',
}
🎨 三、自定义节点组件
3.1 节点类型定义
// src/types/workflow.ts
export type NodeType =
| 'trigger' // 触发器节点
| 'action' // 操作节点
| 'condition' // 条件节点
| 'loop' // 循环节点
| 'transform'; // 转换节点
export interface WorkflowNodeData {
label: string;
description?: string;
icon?: string;
config?: Record<string, any>;
connector?: string;
status?: 'idle' | 'running' | 'success' | 'error';
}
3.2 触发器节点组件
// src/features/workflows/components/nodes/TriggerNode.tsx
import { memo } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { Zap } from 'lucide-react';
import type { WorkflowNodeData } from '@types/workflow';
function TriggerNode({ data, selected }: NodeProps<WorkflowNodeData>) {
return (
<div
className={`
px-4 py-3 rounded-lg border-2 bg-white shadow-md
min-w-[200px] transition-all duration-200
${selected ? 'border-node-trigger ring-2 ring-node-trigger ring-opacity-50' : 'border-gray-300'}
hover:shadow-lg
`}
>
{/* 节点头部 */}
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 rounded-full bg-node-trigger bg-opacity-10 flex items-center justify-center">
<Zap className="w-4 h-4 text-node-trigger" />
</div>
<div className="flex-1">
<div className="text-xs text-gray-500 uppercase font-semibold">触发器</div>
<div className="text-sm font-medium text-gray-900">{data.label}</div>
</div>
</div>
{/* 节点描述 */}
{data.description && (
<div className="text-xs text-gray-600 mb-2">{data.description}</div>
)}
{/* 连接器信息 */}
{data.connector && (
<div className="text-xs text-gray-500 bg-gray-50 px-2 py-1 rounded">
{data.connector}
</div>
)}
{/* 状态指示器 */}
{data.status && (
<div className="mt-2 flex items-center gap-1">
<div
className={`
w-2 h-2 rounded-full
${data.status === 'running' ? 'bg-blue-500 animate-pulse' : ''}
${data.status === 'success' ? 'bg-green-500' : ''}
${data.status === 'error' ? 'bg-red-500' : ''}
${data.status === 'idle' ? 'bg-gray-300' : ''}
`}
/>
<span className="text-xs text-gray-600 capitalize">{data.status}</span>
</div>
)}
{/* 输出连接点 */}
<Handle
type="source"
position={Position.Bottom}
className="w-3 h-3 !bg-node-trigger border-2 border-white"
/>
</div>
);
}
export default memo(TriggerNode);
3.3 操作节点组件
// src/features/workflows/components/nodes/ActionNode.tsx
import { memo } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { Play } from 'lucide-react';
import type { WorkflowNodeData } from '@types/workflow';
function ActionNode({ data, selected }: NodeProps<WorkflowNodeData>) {
return (
<div
className={`
px-4 py-3 rounded-lg border-2 bg-white shadow-md
min-w-[200px] transition-all duration-200
${selected ? 'border-node-action ring-2 ring-node-action ring-opacity-50' : 'border-gray-300'}
hover:shadow-lg
`}
>
{/* 输入连接点 */}
<Handle
type="target"
position={Position.Top}
className="w-3 h-3 !bg-node-action border-2 border-white"
/>
{/* 节点头部 */}
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 rounded-full bg-node-action bg-opacity-10 flex items-center justify-center">
<Play className="w-4 h-4 text-node-action" />
</div>
<div className="flex-1">
<div className="text-xs text-gray-500 uppercase font-semibold">操作</div>
<div className="text-sm font-medium text-gray-900">{data.label}</div>
</div>
</div>
{/* 节点描述 */}
{data.description && (
<div className="text-xs text-gray-600 mb-2">{data.description}</div>
)}
{/* 连接器信息 */}
{data.connector && (
<div className="text-xs text-gray-500 bg-gray-50 px-2 py-1 rounded">
{data.connector}
</div>
)}
{/* 输出连接点 */}
<Handle
type="source"
position={Position.Bottom}
className="w-3 h-3 !bg-node-action border-2 border-white"
/>
</div>
);
}
export default memo(ActionNode);
3.4 条件节点组件
// src/features/workflows/components/nodes/ConditionNode.tsx
import { memo } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { GitBranch } from 'lucide-react';
import type { WorkflowNodeData } from '@types/workflow';
function ConditionNode({ data, selected }: NodeProps<WorkflowNodeData>) {
return (
<div
className={`
px-4 py-3 rounded-lg border-2 bg-white shadow-md
min-w-[200px] transition-all duration-200
${selected ? 'border-node-condition ring-2 ring-node-condition ring-opacity-50' : 'border-gray-300'}
hover:shadow-lg
`}
>
{/* 输入连接点 */}
<Handle
type="target"
position={Position.Top}
className="w-3 h-3 !bg-node-condition border-2 border-white"
/>
{/* 节点头部 */}
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 rounded-full bg-node-condition bg-opacity-10 flex items-center justify-center">
<GitBranch className="w-4 h-4 text-node-condition" />
</div>
<div className="flex-1">
<div className="text-xs text-gray-500 uppercase font-semibold">条件</div>
<div className="text-sm font-medium text-gray-900">{data.label}</div>
</div>
</div>
{/* 节点描述 */}
{data.description && (
<div className="text-xs text-gray-600 mb-2">{data.description}</div>
)}
{/* 多个输出连接点 */}
<Handle
type="source"
position={Position.Bottom}
id="true"
className="w-3 h-3 !bg-green-500 border-2 border-white !left-[30%]"
/>
<Handle
type="source"
position={Position.Bottom}
id="false"
className="w-3 h-3 !bg-red-500 border-2 border-white !left-[70%]"
/>
</div>
);
}
export default memo(ConditionNode);
3.5 注册自定义节点
// src/features/workflows/components/WorkflowEditor.tsx
import { useMemo } from 'react';
import ReactFlow, { NodeTypes } from 'reactflow';
import TriggerNode from './nodes/TriggerNode';
import ActionNode from './nodes/ActionNode';
import ConditionNode from './nodes/ConditionNode';
import LoopNode from './nodes/LoopNode';
import TransformNode from './nodes/TransformNode';
export default function WorkflowEditor() {
// 注册自定义节点类型
const nodeTypes: NodeTypes = useMemo(
() => ({
trigger: TriggerNode,
action: ActionNode,
condition: ConditionNode,
loop: LoopNode,
transform: TransformNode,
}),
[]
);
return (
<ReactFlow
nodeTypes={nodeTypes}
// ... 其他props
/>
);
}
🔗 四、自定义边组件
4.1 基础自定义边
// src/features/workflows/components/edges/CustomEdge.tsx
import { memo } from 'react';
import {
EdgeProps,
getBezierPath,
EdgeLabelRenderer,
BaseEdge,
} from 'reactflow';
function CustomEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
label,
selected,
}: EdgeProps) {
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
...style,
strokeWidth: selected ? 3 : 2,
stroke: selected ? '#3b82f6' : '#94a3b8',
}}
/>
{label && (
<EdgeLabelRenderer>
<div
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
pointerEvents: 'all',
}}
className="nodrag nopan"
>
<div className="bg-white px-2 py-1 rounded shadow-md text-xs border border-gray-200">
{label}
</div>
</div>
</EdgeLabelRenderer>
)}
</>
);
}
export default memo(CustomEdge);
4.2 条件边组件
// src/features/workflows/components/edges/ConditionalEdge.tsx
import { memo } from 'react';
import {
EdgeProps,
getBezierPath,
EdgeLabelRenderer,
BaseEdge,
} from 'reactflow';
interface ConditionalEdgeData {
condition?: 'true' | 'false';
}
function ConditionalEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
data,
selected,
}: EdgeProps<ConditionalEdgeData>) {
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const isTrue = data?.condition === 'true';
const isFalse = data?.condition === 'false';
return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
...style,
strokeWidth: selected ? 3 : 2,
stroke: isTrue ? '#10b981' : isFalse ? '#ef4444' : '#94a3b8',
strokeDasharray: isTrue || isFalse ? '5,5' : 'none',
}}
/>
<EdgeLabelRenderer>
<div
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
pointerEvents: 'all',
}}
className="nodrag nopan"
>
<div
className={`
px-2 py-1 rounded shadow-md text-xs border font-medium
${isTrue ? 'bg-green-50 border-green-200 text-green-700' : ''}
${isFalse ? 'bg-red-50 border-red-200 text-red-700' : ''}
${!isTrue && !isFalse ? 'bg-white border-gray-200 text-gray-700' : ''}
`}
>
{isTrue ? '✓ True' : isFalse ? '✗ False' : 'Condition'}
</div>
</div>
</EdgeLabelRenderer>
</>
);
}
export default memo(ConditionalEdge);
4.3 注册自定义边
// src/features/workflows/components/WorkflowEditor.tsx
import { useMemo } from 'react';
import ReactFlow, { EdgeTypes } from 'reactflow';
import CustomEdge from './edges/CustomEdge';
import ConditionalEdge from './edges/ConditionalEdge';
export default function WorkflowEditor() {
const edgeTypes: EdgeTypes = useMemo(
() => ({
custom: CustomEdge,
conditional: ConditionalEdge,
}),
[]
);
return (
<ReactFlow
edgeTypes={edgeTypes}
defaultEdgeOptions={{
type: 'custom',
animated: false,
}}
// ... 其他props
/>
);
}
🎯 五、拖拽功能实现
5.1 节点面板组件
// src/features/workflows/components/NodePanel.tsx
import { DragEvent } from 'react';
import { Zap, Play, GitBranch, Repeat, Shuffle } from 'lucide-react';
interface NodeTemplate {
type: string;
label: string;
icon: React.ReactNode;
description: string;
color: string;
}
const nodeTemplates: NodeTemplate[] = [
{
type: 'trigger',
label: '触发器',
icon: <Zap className="w-5 h-5" />,
description: '工作流的起点',
color: 'bg-node-trigger',
},
{
type: 'action',
label: '操作',
icon: <Play className="w-5 h-5" />,
description: '执行具体操作',
color: 'bg-node-action',
},
{
type: 'condition',
label: '条件',
icon: <GitBranch className="w-5 h-5" />,
description: '条件分支',
color: 'bg-node-condition',
},
{
type: 'loop',
label: '循环',
icon: <Repeat className="w-5 h-5" />,
description: '循环执行',
color: 'bg-node-loop',
},
{
type: 'transform',
label: '转换',
icon: <Shuffle className="w-5 h-5" />,
description: '数据转换',
color: 'bg-node-transform',
},
];
export default function NodePanel() {
const onDragStart = (event: DragEvent, nodeType: string) => {
event.dataTransfer.setData('application/reactflow', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
return (
<div className="w-64 bg-white border-r border-gray-200 p-4">
<h3 className="text-sm font-semibold text-gray-900 mb-4">节点库</h3>
<div className="space-y-2">
{nodeTemplates.map((template) => (
<div
key={template.type}
draggable
onDragStart={(e) => onDragStart(e, template.type)}
className="
flex items-center gap-3 p-3 rounded-lg border-2 border-gray-200
cursor-move hover:border-primary-300 hover:bg-primary-50
transition-all duration-200
"
>
<div className={`w-10 h-10 rounded-lg ${template.color} bg-opacity-10 flex items-center justify-center`}>
<div className={`${template.color.replace('bg-', 'text-')}`}>
{template.icon}
</div>
</div>
<div className="flex-1">
<div className="text-sm font-medium text-gray-900">
{template.label}
</div>
<div className="text-xs text-gray-500">
{template.description}
</div>
</div>
</div>
))}
</div>
</div>
);
}
5.2 处理拖放事件
// src/features/workflows/components/WorkflowEditor.tsx
import { useCallback, useRef, DragEvent } from 'react';
import ReactFlow, { ReactFlowInstance } from 'reactflow';
import { v4 as uuidv4 } from 'uuid';
export default function WorkflowEditor() {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance | null>(null);
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// 拖拽进入画布
const onDragOver = useCallback((event: DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, []);
// 放置节点
const onDrop = useCallback(
(event: DragEvent) => {
event.preventDefault();
const type = event.dataTransfer.getData('application/reactflow');
if (typeof type === 'undefined' || !type || !reactFlowInstance) {
return;
}
// 计算节点位置(相对于画布)
const position = reactFlowInstance.screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
// 创建新节点
const newNode = {
id: uuidv4(),
type,
position,
data: {
label: `${type.charAt(0).toUpperCase() + type.slice(1)} Node`,
description: '点击配置节点',
},
};
setNodes((nds) => nds.concat(newNode));
},
[reactFlowInstance, setNodes]
);
return (
<div className="flex h-screen">
<NodePanel />
<div className="flex-1" ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onInit={setReactFlowInstance}
onDrop={onDrop}
onDragOver={onDragOver}
// ... 其他props
/>
</div>
</div>
);
}
🔍 六、画布交互优化
6.1 缩放与平移配置
// src/features/workflows/components/WorkflowEditor.tsx
import ReactFlow, {
PanOnScrollMode,
SelectionMode,
} from 'reactflow';
export default function WorkflowEditor() {
return (
<ReactFlow
// 缩放配置
minZoom={0.1} // 最小缩放比例
maxZoom={2} // 最大缩放比例
defaultZoom={1} // 默认缩放比例
// 平移配置
panOnScroll={true} // 滚动时平移
panOnScrollMode={PanOnScrollMode.Free} // 平移模式
panOnScrollSpeed={0.5} // 平移速度
// 选择配置
selectionMode={SelectionMode.Partial} // 部分选择模式
selectionOnDrag={true} // 拖拽时选择
// 多选配置
multiSelectionKeyCode="Shift" // 多选按键
deleteKeyCode="Delete" // 删除按键
// 自动适应视图
fitView
fitViewOptions={{
padding: 0.2, // 边距
includeHiddenNodes: false, // 不包含隐藏节点
}}
// ... 其他props
/>
);
}
6.2 自定义控制器
// src/features/workflows/components/CustomControls.tsx
import { Panel, useReactFlow } from 'reactflow';
import {
ZoomIn,
ZoomOut,
Maximize,
Lock,
Unlock,
Grid3x3,
} from 'lucide-react';
import { useState } from 'react';
export default function CustomControls() {
const { zoomIn, zoomOut, fitView } = useReactFlow();
const [isLocked, setIsLocked] = useState(false);
const [showGrid, setShowGrid] = useState(true);
return (
<Panel position="top-right" className="space-y-2">
{/* 缩放控制 */}
<div className="bg-white rounded-lg shadow-md p-1 space-y-1">
<button
onClick={() => zoomIn()}
className="w-8 h-8 flex items-center justify-center hover:bg-gray-100 rounded"
title="放大"
>
<ZoomIn className="w-4 h-4" />
</button>
<button
onClick={() => zoomOut()}
className="w-8 h-8 flex items-center justify-center hover:bg-gray-100 rounded"
title="缩小"
>
<ZoomOut className="w-4 h-4" />
</button>
<button
onClick={() => fitView({ padding: 0.2 })}
className="w-8 h-8 flex items-center justify-center hover:bg-gray-100 rounded"
title="适应视图"
>
<Maximize className="w-4 h-4" />
</button>
</div>
{/* 画布控制 */}
<div className="bg-white rounded-lg shadow-md p-1 space-y-1">
<button
onClick={() => setIsLocked(!isLocked)}
className={`
w-8 h-8 flex items-center justify-center rounded
${isLocked ? 'bg-primary-100 text-primary-600' : 'hover:bg-gray-100'}
`}
title={isLocked ? '解锁画布' : '锁定画布'}
>
{isLocked ? <Lock className="w-4 h-4" /> : <Unlock className="w-4 h-4" />}
</button>
<button
onClick={() => setShowGrid(!showGrid)}
className={`
w-8 h-8 flex items-center justify-center rounded
${showGrid ? 'bg-primary-100 text-primary-600' : 'hover:bg-gray-100'}
`}
title={showGrid ? '隐藏网格' : '显示网格'}
>
<Grid3x3 className="w-4 h-4" />
</button>
</div>
</Panel>
);
}
6.3 小地图导航
// src/features/workflows/components/WorkflowEditor.tsx
import ReactFlow, { MiniMap } from 'reactflow';
export default function WorkflowEditor() {
return (
<ReactFlow
// ... 其他props
>
<MiniMap
nodeColor={(node) => {
switch (node.type) {
case 'trigger':
return '#8b5cf6';
case 'action':
return '#3b82f6';
case 'condition':
return '#f59e0b';
case 'loop':
return '#10b981';
case 'transform':
return '#ec4899';
default:
return '#94a3b8';
}
}}
nodeStrokeWidth={3}
zoomable
pannable
className="!bg-gray-50 !border-2 !border-gray-200 !rounded-lg"
/>
</ReactFlow>
);
}
✅ 七、节点连接验证
7.1 连接规则定义
// src/features/workflows/utils/connectionRules.ts
import { Connection, Node } from 'reactflow';
/**
* 连接验证规则
*/
export const connectionRules = {
// 触发器节点只能作为源节点
trigger: {
canBeSource: true,
canBeTarget: false,
maxOutgoing: Infinity,
maxIncoming: 0,
},
// 操作节点可以作为源和目标
action: {
canBeSource: true,
canBeTarget: true,
maxOutgoing: Infinity,
maxIncoming: Infinity,
},
// 条件节点有两个输出
condition: {
canBeSource: true,
canBeTarget: true,
maxOutgoing: 2,
maxIncoming: Infinity,
requiredHandles: ['true', 'false'],
},
// 循环节点
loop: {
canBeSource: true,
canBeTarget: true,
maxOutgoing: Infinity,
maxIncoming: Infinity,
},
// 转换节点
transform: {
canBeSource: true,
canBeTarget: true,
maxOutgoing: Infinity,
maxIncoming: Infinity,
},
};
/**
* 验证连接是否合法
*/
export function isValidConnection(
connection: Connection,
nodes: Node[],
edges: any[]
): boolean {
const { source, target, sourceHandle, targetHandle } = connection;
if (!source || !target) return false;
// 不能连接到自己
if (source === target) return false;
// 查找源节点和目标节点
const sourceNode = nodes.find((n) => n.id === source);
const targetNode = nodes.find((n) => n.id === target);
if (!sourceNode || !targetNode) return false;
// 获取节点类型规则
const sourceRule = connectionRules[sourceNode.type as keyof typeof connectionRules];
const targetRule = connectionRules[targetNode.type as keyof typeof connectionRules];
if (!sourceRule || !targetRule) return true; // 未知类型,允许连接
// 检查源节点是否可以作为源
if (!sourceRule.canBeSource) return false;
// 检查目标节点是否可以作为目标
if (!targetRule.canBeTarget) return false;
// 检查源节点的输出数量限制
const outgoingEdges = edges.filter((e) => e.source === source);
if (outgoingEdges.length >= sourceRule.maxOutgoing) return false;
// 检查目标节点的输入数量限制
const incomingEdges = edges.filter((e) => e.target === target);
if (incomingEdges.length >= targetRule.maxIncoming) return false;
// 检查是否已存在相同的连接
const duplicateEdge = edges.find(
(e) =>
e.source === source &&
e.target === target &&
e.sourceHandle === sourceHandle &&
e.targetHandle === targetHandle
);
if (duplicateEdge) return false;
// 检查是否会形成循环(简单检测)
if (wouldCreateCycle(source, target, edges)) return false;
return true;
}
/**
* 检测是否会形成循环
*/
function wouldCreateCycle(
source: string,
target: string,
edges: any[]
): boolean {
const visited = new Set<string>();
const stack = [target];
while (stack.length > 0) {
const current = stack.pop()!;
if (current === source) return true;
if (visited.has(current)) continue;
visited.add(current);
const outgoing = edges.filter((e) => e.source === current);
outgoing.forEach((e) => stack.push(e.target));
}
return false;
}
7.2 应用连接验证
// src/features/workflows/components/WorkflowEditor.tsx
import { useCallback } from 'react';
import ReactFlow, { Connection, Edge } from 'reactflow';
import { isValidConnection } from '../utils/connectionRules';
export default function WorkflowEditor() {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// 验证连接
const isValidConnectionHandler = useCallback(
(connection: Connection) => {
return isValidConnection(connection, nodes, edges);
},
[nodes, edges]
);
// 连接时的回调
const onConnect = useCallback(
(connection: Connection) => {
if (isValidConnection(connection, nodes, edges)) {
setEdges((eds) => addEdge(connection, eds));
} else {
// 显示错误提示
console.warn('Invalid connection:', connection);
// 可以添加 toast 提示
}
},
[nodes, edges, setEdges]
);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
isValidConnection={isValidConnectionHandler}
// ... 其他props
/>
);
}
🎨 八、完整的编辑器组件
// src/features/workflows/components/WorkflowEditor.tsx
import { useCallback, useRef, useState, DragEvent } from 'react';
import ReactFlow, {
ReactFlowProvider,
ReactFlowInstance,
Node,
Edge,
Connection,
Controls,
Background,
BackgroundVariant,
MiniMap,
Panel,
useNodesState,
useEdgesState,
addEdge,
NodeTypes,
EdgeTypes,
ConnectionMode,
} from 'reactflow';
import 'reactflow/dist/style.css';
// 组件导入
import NodePanel from './NodePanel';
import CustomControls from './CustomControls';
import TriggerNode from './nodes/TriggerNode';
import ActionNode from './nodes/ActionNode';
import ConditionNode from './nodes/ConditionNode';
import LoopNode from './nodes/LoopNode';
import TransformNode from './nodes/TransformNode';
import CustomEdge from './edges/CustomEdge';
import ConditionalEdge from './edges/ConditionalEdge';
// 工具函数
import { isValidConnection } from '../utils/connectionRules';
import { v4 as uuidv4 } from 'uuid';
function WorkflowEditorContent() {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance | null>(null);
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// 注册自定义节点类型
const nodeTypes: NodeTypes = {
trigger: TriggerNode,
action: ActionNode,
condition: ConditionNode,
loop: LoopNode,
transform: TransformNode,
};
// 注册自定义边类型
const edgeTypes: EdgeTypes = {
custom: CustomEdge,
conditional: ConditionalEdge,
};
// 拖放处理
const onDragOver = useCallback((event: DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, []);
const onDrop = useCallback(
(event: DragEvent) => {
event.preventDefault();
const type = event.dataTransfer.getData('application/reactflow');
if (!type || !reactFlowInstance) return;
const position = reactFlowInstance.screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
const newNode: Node = {
id: uuidv4(),
type,
position,
data: {
label: `${type.charAt(0).toUpperCase() + type.slice(1)} Node`,
description: '点击配置节点',
},
};
setNodes((nds) => nds.concat(newNode));
},
[reactFlowInstance, setNodes]
);
// 连接验证
const isValidConnectionHandler = useCallback(
(connection: Connection) => isValidConnection(connection, nodes, edges),
[nodes, edges]
);
// 连接处理
const onConnect = useCallback(
(connection: Connection) => {
if (isValidConnection(connection, nodes, edges)) {
setEdges((eds) => addEdge(connection, eds));
}
},
[nodes, edges, setEdges]
);
return (
<div className="flex h-screen bg-gray-50">
{/* 节点面板 */}
<NodePanel />
{/* 画布区域 */}
<div className="flex-1" ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onInit={setReactFlowInstance}
onDrop={onDrop}
onDragOver={onDragOver}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
isValidConnection={isValidConnectionHandler}
connectionMode={ConnectionMode.Loose}
minZoom={0.1}
maxZoom={2}
defaultZoom={1}
fitView
fitViewOptions={{ padding: 0.2 }}
>
{/* 控制器 */}
<Controls />
<CustomControls />
{/* 背景 */}
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
{/* 小地图 */}
<MiniMap
nodeColor={(node) => {
const colors: Record<string, string> = {
trigger: '#8b5cf6',
action: '#3b82f6',
condition: '#f59e0b',
loop: '#10b981',
transform: '#ec4899',
};
return colors[node.type || 'default'] || '#94a3b8';
}}
nodeStrokeWidth={3}
zoomable
pannable
className="!bg-gray-50 !border-2 !border-gray-200 !rounded-lg"
/>
{/* 顶部工具栏 */}
<Panel position="top-left">
<div className="bg-white rounded-lg shadow-md px-4 py-2">
<h2 className="text-lg font-semibold text-gray-900">
工作流编辑器
</h2>
</div>
</Panel>
</ReactFlow>
</div>
</div>
);
}
// 包装Provider
export default function WorkflowEditor() {
return (
<ReactFlowProvider>
<WorkflowEditorContent />
</ReactFlowProvider>
);
}
💡 九、性能优化技巧
9.1 节点渲染优化
/**
* 性能优化最佳实践
*/
// 1. 使用 memo 避免不必要的重渲染
import { memo } from 'react';
const TriggerNode = memo(({ data, selected }: NodeProps) => {
// 组件实现
});
// 2. 使用 useMemo 缓存计算结果
const nodeTypes = useMemo(
() => ({
trigger: TriggerNode,
action: ActionNode,
// ...
}),
[] // 空依赖数组,只创建一次
);
// 3. 使用 useCallback 缓存回调函数
const onConnect = useCallback(
(connection: Connection) => {
// 处理逻辑
},
[dependencies] // 只在依赖变化时重新创建
);
// 4. 虚拟化大型工作流
<ReactFlow
onlyRenderVisibleElements={true} // 只渲染可见元素
/>
// 5. 减少状态更新频率
const onNodesChange = useCallback(
(changes) => {
// 批量处理变更
setNodes((nds) => applyNodeChanges(changes, nds));
},
[]
);
9.2 大型工作流优化
// src/features/workflows/hooks/useOptimizedWorkflow.ts
import { useEffect } from 'react';
import { useReactFlow } from 'reactflow';
export function useOptimizedWorkflow(nodeCount: number) {
const { setOptions } = useReactFlow();
useEffect(() => {
// 节点数量超过100时启用优化
if (nodeCount > 100) {
setOptions({
// 只渲染可见元素
onlyRenderVisibleElements: true,
// 减少更新频率
nodesDraggable: true,
nodesConnectable: true,
elementsSelectable: true,
});
}
}, [nodeCount, setOptions]);
}
💡 本文小结
核心要点回顾:
-
React Flow选型优势:
- React原生集成,学习成本适中
- 高度可定制化,满足复杂需求
- 性能优秀,支持大型工作流
-
自定义节点开发:
- 使用 memo 优化性能
- 合理设计节点数据结构
- 提供清晰的视觉反馈
-
自定义边组件:
- 使用 getBezierPath 计算路径
- EdgeLabelRenderer 渲染标签
- 支持条件边等特殊类型
-
拖拽交互:
- 节点面板提供拖拽源
- 画布处理拖放事件
- 自动计算节点位置
-
连接验证:
- 定义连接规则
- 防止无效连接
- 检测循环依赖
-
性能优化:
- 使用 memo/useMemo/useCallback
- 启用虚拟化渲染
- 批量处理状态更新
下一篇预告:
在下一篇文章中,我们将继续深入可视化编辑器的开发,重点实现:
- 节点配置面板(动态表单生成)
- 数据映射UI(变量引用系统)
- 实时预览功能
- 工作流保存与加载
📦 本文资源
完整代码:
# 克隆示例代码
git clone https://github.com/quantumflow/workflow-editor-demo.git
cd workflow-editor-demo
npm install
npm run dev
配置文件:
- ✅
WorkflowEditor.tsx- 完整编辑器组件 - ✅
nodes/- 所有自定义节点组件 - ✅
edges/- 自定义边组件 - ✅
utils/connectionRules.ts- 连接验证规则
🤔 思考题
-
节点设计:如何设计一个支持多输入多输出的复杂节点?
-
性能优化:当工作流包含1000+节点时,如何保证流畅的交互体验?
-
撤销重做:如何实现工作流编辑器的撤销/重做功能?
-
自动布局:如何实现工作流的自动布局算法?
-
协同编辑:如何实现多人实时协同编辑工作流?
欢迎在评论区分享你的思考和实现方案!
作者:DREAMVFIA
专注于企业级应用架构设计与工作流自动化技术
版权声明:
本文为QuantumFlow专栏原创内容,遵循MIT开源协议。欢迎转载,请注明出处。
更多推荐



所有评论(0)