📚 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]);
}

💡 本文小结

核心要点回顾:

  1. React Flow选型优势

    • React原生集成,学习成本适中
    • 高度可定制化,满足复杂需求
    • 性能优秀,支持大型工作流
  2. 自定义节点开发

    • 使用 memo 优化性能
    • 合理设计节点数据结构
    • 提供清晰的视觉反馈
  3. 自定义边组件

    • 使用 getBezierPath 计算路径
    • EdgeLabelRenderer 渲染标签
    • 支持条件边等特殊类型
  4. 拖拽交互

    • 节点面板提供拖拽源
    • 画布处理拖放事件
    • 自动计算节点位置
  5. 连接验证

    • 定义连接规则
    • 防止无效连接
    • 检测循环依赖
  6. 性能优化

    • 使用 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 - 连接验证规则


🤔 思考题

  1. 节点设计:如何设计一个支持多输入多输出的复杂节点?

  2. 性能优化:当工作流包含1000+节点时,如何保证流畅的交互体验?

  3. 撤销重做:如何实现工作流编辑器的撤销/重做功能?

  4. 自动布局:如何实现工作流的自动布局算法?

  5. 协同编辑:如何实现多人实时协同编辑工作流?

欢迎在评论区分享你的思考和实现方案!


作者:DREAMVFIA
专注于企业级应用架构设计与工作流自动化技术

版权声明:
本文为QuantumFlow专栏原创内容,遵循MIT开源协议。欢迎转载,请注明出处。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐