浅谈工作流画布引擎 React Flow

React Flow 作为基于 React 的现代化工作流编排工具,通过其声明式渲染协议和可扩展的节点系统,已成为百宝箱、扣子等主流工作流平台的核心画布引擎, 服务于智能体的搭建。

一、React Flow 简介

React Flow 是一个用于构建基于节点的应用程序的库。官方文档 reactflow.dev/learn 可以实现自定义节点类型和边,它还视口控件等等组件。

  1. 易于使用:已经具备了许多开箱即用的功能。拖动节点、缩放和平移、选择多个节点和边缘以及添加/删除边都是内置功能。
  2. 可定制:支持自定义节点类型和边类型。由于自定义节点只是 React 组件,因此你可以实现任何需要的功能,不会被锁定在内置的节点类型中。 自定义边可让你在节点边添加标签、控件和定制逻辑。
  3. 快速渲染:只渲染发生变化的节点,并确保只显示视口中的节点。
  4. 内置插件 :提供了一些开箱即用的插件,如 <Background /> 可实现一些基本的自定义背景图案;<MiniMap /> 可在屏幕一角显示小版本的图形;<Controls /> 可添加缩放、居中和锁定视口的控件;<Panel /> 可让你轻松将内容放置在视口的顶部;<NodeToolbar /> 可让你呈现连接到节点的工具栏;<NodeResizer /> 可让你轻松为节点添加调整大小的功能。

二、使用步骤

(一)安装

sql 复制代码
yarn add @xyflow/react
npm install @xyflow/react

(二)创建第一个 Flow

reactflow 软件包导出 <ReactFlow /> 组件作为默认导出。加上一些节点和边,就可以开始工作了。

javascript 复制代码
import React from 'react';
import { ReactFlow } from '@xyflow/react';
 
import '@xyflow/react/dist/style.css';

// 初始化节点
// 节点唯一 ID 
const initialNodes = [
  { id: '1', position: { x: 0, y: 0 }, data: { label: '1' } },
  { id: '2', position: { x: 0, y: 100 }, data: { label: '2' } },
];
// 初始化边 
// 边存在唯一 ID
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }];
 
export default function App() {
  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <ReactFlow nodes={initialNodes} edges={initialEdges} />
    </div>
  );
}

(三)节点 & 边介绍

1.1 自定义节点

javascript 复制代码
function TextUpdaterNode(props) {
  const onChange = useCallback((evt) => {
    console.log(evt.target.value);
  }, []);
 
  return (
    <div className="text-updater-node">
      <div>
        <label htmlFor="text">Text:</label>
        <input id="text" name="text" onChange={onChange} className="nodrag" />
      </div>
    </div>
  );
}

1.2 注册节点

ini 复制代码
const nodeTypes = {
  textUpdater: TextUpdaterNode
};
 
function Flow() {
  ...
  return (
    <ReactFlow
      nodes={nodes}
      edges={edges}
      nodeTypes={nodeTypes}
      ...
    />
  );
}

1.3 消费自定义节点

yaml 复制代码
const nodes = [
  {
    id: 'node-1',
    type: 'textUpdater',
    position: { x: 0, y: 0 },
    data: { value: 123 },
  },
];

批量设置节点 setNodes

javascript 复制代码
export function useNodesState<NodeType extends Node>(
  initialNodes: NodeType[]
): [
  nodes: NodeType[],
  setNodes: Dispatch<SetStateAction<NodeType[]>>,
  onNodesChange: OnNodesChange<NodeType>
] {
  const [nodes, setNodes] = useState(initialNodes);
  const onNodesChange: OnNodesChange<NodeType> = useCallback(
    (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
    []
  );

  return [nodes, setNodes, onNodesChange];
}

1.4 自定义边

javascript 复制代码
import React from 'react';
import ReactFlow, { BaseEdge, EdgeLabelRenderer } from 'reactflow';

// 1. 定义自定义边组件
const CustomEdge = ({
  id,
  sourceX,
  sourceY,
  targetX,
  targetY,
  sourcePosition,
  targetPosition,
  style = {},
  markerEnd,
}) => {
  const [edgePath, labelX, labelY] = getCustomEdge({
    sourceX,
    sourceY,
    sourcePosition,
    targetX,
    targetY,
    targetPosition,
  });

  return (
    <>
      <BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
      <EdgeLabelRenderer>
        <div
          style={{
            position: 'absolute',
            transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
            background: '#ffcc00',
            padding: 5,
            borderRadius: 5,
            fontSize: 12,
            fontWeight: 700,
          }}
          className="nodrag nopan"
        >
          自定义边
        </div>
      </EdgeLabelRenderer>
    </>
  );
};

1.5 边注册

ini 复制代码
// 2. 定义边类型
const edgeTypes = {
  custom: CustomEdge,
};

// 3. 在ReactFlow中使用
function Flow() {
  return (
    <div style={{ width: '100%', height: '500px' }}>
      <ReactFlow 
        edgeTypes={edgeTypes}
        nodes={[...]}
        edges={[...]}
      />
    </div>
  );
}

批量设置边 setEdges

ini 复制代码
export function useEdgesState<EdgeType extends Edge = Edge>(
  initialEdges: EdgeType[]
): [
  //
  edges: EdgeType[],
  setEdges: Dispatch<SetStateAction<EdgeType[]>>,
  onEdgesChange: OnEdgesChange<EdgeType>
] {
  const [edges, setEdges] = useState(initialEdges);
  const onEdgesChange: OnEdgesChange<EdgeType> = useCallback(
    (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
    []
  );

  return [edges, setEdges, onEdgesChange];
}

(四)边事件详解

React Flow 提供了多种边(Edge)相关的事件回调,用于处理用户与边的交互行为。以下是核心边事件及其用法:

1.1 onEdgeClick

  • 触发时机:用户单击边时触发。

  • 参数

    • event: 鼠标事件对象
    • edge: 被点击的边对象(包含 idsourcetarget 等属性)
  • 典型用途

    • 显示边的详细信息(如弹窗)
    • 选中边并高亮关联节点
    jsx 复制代码
    const onEdgeClick = (event, edge) => {
      console.log('Clicked edge:', edge);
      // 示例:显示边属性弹窗
      setSelectedEdge(edge);
    };

1.2 onEdgeDoubleClick

  • 触发时机:用户双击边时触发。

  • 参数 :同 onEdgeClick

  • 典型用途

    • 快速编辑边属性
    • 删除边(需配合确认逻辑)
    jsx 复制代码
    const onEdgeDoubleClick = (event, edge) => {
      if (confirm('确认删除此边?')) {
        setEdges(eds => eds.filter(e => e.id !== edge.id));
      }
    };

1.3 onEdgeMouseEnter / onEdgeMouseLeave

  • 触发时机:鼠标悬停/离开边时触发。

  • 参数 :同 onEdgeClick

  • 典型用途

    • 动态修改边样式(如颜色、虚线)
    • 显示悬停提示(Tooltip)
    jsx 复制代码
    const [hoveredEdgeId, setHoveredEdgeId] = useState(null);
    const onEdgeMouseEnter = (event, edge) => setHoveredEdgeId(edge.id);
    const onEdgeMouseLeave = () => setHoveredEdgeId(null);
    
    // 动态样式
    const edgesWithStyle = edges.map(edge => ({
      ...edge,
      style: hoveredEdgeId === edge.id ? { stroke: 'red' } : {}
    }));

1.4 onEdgesDelete

  • 触发时机:边被删除时触发(通过键盘 Delete 键或编程删除)。

  • 参数 :被删除的边数组 Edge[]

  • 典型用途

    • 清理与边关联的数据
    • 记录删除操作日志
    jsx 复制代码
    const onEdgesDelete = (deletedEdges) => {
      console.log('Deleted edges:', deletedEdges);
      // 同步更新后端数据
    };

1.5 onEdgesChange

  • 触发时机:边状态变化时触发(如选中、取消选中)。

  • 参数 :边变更数组 EdgeChange[]

  • 典型用途

    • 受控模式下同步边状态
    • useEdgesState 钩子配合使用
    jsx 复制代码
    const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
    <ReactFlow onEdgesChange={onEdgesChange} />

2.1 onConnect

  • 触发时机:用户成功连接两个节点时触发。

  • 参数 :连接对象 Connection(包含 sourcetargetsourceHandle 等)

  • 典型用途

    • 将连接转换为边(需配合 addEdge 工具)
    • 验证连接合法性(如禁止循环连接)
    jsx 复制代码
    const onConnect = (connection) => {
      const newEdge = { ...connection, id: `edge-${Date.now()}` };
      setEdges(eds => addEdge(newEdge, eds));
    };
    // setEdges 边设置方法

2.2 onReconnect

  • 触发时机:用户拖动边的端点重新连接到其他节点时触发。

  • 参数

    • oldEdge: 原边对象
    • newConnection: 新连接对象
  • 典型用途

    • 更新边数据源/目标
    • 检查连接有效性(如端口类型匹配)
    jsx 复制代码
    const onReconnect = (oldEdge, newConnection) => {
      setEdges(eds => eds.map(edge => 
        edge.id === oldEdge.id ? { ...edge, ...newConnection } : edge
      ));
    };

3.1 isValidConnection

  • 触发时机:用户尝试建立连接时实时验证。

  • 参数 :连接对象 Connection

  • 返回booleantrue 表示允许连接)

  • 典型用途

    • 禁止自连接
    • 限制特定类型节点的连接
    jsx 复制代码
    const isValidConnection = (connection) => {
      return connection.source !== connection.target; // 禁止自连接
    };

3.2 onEdgeUpdateStart / onEdgeUpdateEnd

  • 触发时机:开始/结束拖动边端点时触发。

  • 参数

    • event: 鼠标事件
    • edge: 被拖动的边
    • handleType: "source" 或 "target"
  • 典型用途

    • 显示拖动提示
    • 记录操作状态
    jsx 复制代码
    const onEdgeUpdateStart = (event, edge, handleType) => {
      console.log(`开始拖动边 ${edge.id} 的 ${handleType} 端点`);
    };

完整示例代码

jsx 复制代码
import { ReactFlow, useEdgesState, addEdge } from 'reactflow';

function FlowComponent() {
  const [edges, setEdges, onEdgesChange] = useEdgesState([]);

  const onConnect = (connection) => 
    setEdges(eds => addEdge({ ...connection, animated: true }, eds));

  const onEdgeClick = (event, edge) => alert(`点击边: ${edge.id}`);

  return (
    <ReactFlow
      edges={edges}
      onEdgesChange={onEdgesChange}
      onConnect={onConnect}
      onEdgeClick={onEdgeClick}
      isValidConnection={(conn) => conn.source !== conn.target}
    />
  );
}

总结

  • 交互控制 :通过 onEdgeClickonEdgeMouseEnter 等实现边的高亮与提示。
  • 数据管理onEdgesChangeuseEdgesState 配合管理边状态。
  • 连接验证isValidConnection 确保连接符合业务规则。
  • 高级功能onReconnect 支持动态调整边端点。

更多细节可参考 React Flow 官方文档

(五) 节点事件详解

核心节点事件概览

ReactFlow 提供了一系列节点相关的事件处理程序,允许开发者对用户的交互行为做出响应。以下是主要的节点事件类型及其触发时机:

  1. onNodeClick - 当用户点击节点时触发
  2. onNodeDoubleClick - 当用户双击节点时触发
  3. onNodeDragStart - 当用户开始拖动节点时触发
  4. onNodeDrag - 当用户拖动节点过程中持续触发
  5. onNodeDragStop - 当用户停止拖动节点时触发
  6. onNodeMouseEnter - 当鼠标进入节点区域时触发
  7. onNodeMouseMove - 当鼠标在节点区域内移动时触发
  8. onNodeMouseLeave - 当鼠标离开节点区域时触发
  9. onNodeContextMenu - 当用户在节点上右键点击时触发
  10. onNodesDelete - 当节点被删除时触发

1.1 点击相关事件

onNodeClick 是最常用的节点事件之一,它允许开发者在用户点击节点时执行自定义逻辑。这个事件接收两个参数:事件对象和被点击的节点对象。

jsx 复制代码
const onNodeClick = useCallback((event, node) => {
  console.log('Node clicked:', node.id, node.data);
  // 可以在这里更新节点状态或执行其他操作
}, []);

onNodeDoubleClick 类似于单击事件,但只在快速连续点击两次时触发。这在需要区分单击和双击操作的场景中非常有用。

1.2 拖拽相关事件

ReactFlow 提供了完整的拖拽生命周期事件,使开发者能够精确控制节点的拖拽行为:

  • onNodeDragStart:拖拽开始时触发,适合用于初始化拖拽状态或记录原始位置
  • onNodeDrag:拖拽过程中持续触发,可用于实时更新相关UI或执行碰撞检测
  • onNodeDragStop:拖拽结束时触发,适合用于提交最终位置或执行验证
jsx 复制代码
const onNodeDragStart = useCallback((event, node) => {
  console.log('Drag started for node:', node.id);
}, []);

const onNodeDrag = useCallback((event, node) => {
  // 实时更新节点位置或其他相关状态
}, []);

const onNodeDragStop = useCallback((event, node) => {
  console.log('Drag ended for node:', node.id, 'at position:', node.position);
}, []);

1.3 鼠标悬停事件

鼠标悬停相关事件对于创建响应式UI非常有用:

  • onNodeMouseEnter:鼠标进入节点区域时触发,适合用于显示工具提示或高亮节点
  • onNodeMouseMove:鼠标在节点内移动时触发,可用于实现精细的鼠标跟踪效果
  • onNodeMouseLeave:鼠标离开节点区域时触发,适合用于清除悬停状态
jsx 复制代码
const [hoveredNode, setHoveredNode] = useState(null);

const onNodeMouseEnter = useCallback((event, node) => {
  setHoveredNode(node.id);
}, []);

const onNodeMouseLeave = useCallback(() => {
  setHoveredNode(null);
}, []);

需要注意的是,在某些情况下,自定义节点可能会出现鼠标事件"闪烁"的问题,即鼠标在节点边缘时快速交替触发enter和leave事件。这通常是由于节点边缘检测区域的问题导致的。

1.4 上下文菜单事件

onNodeContextMenu 允许开发者在节点上实现自定义右键菜单功能。默认情况下,浏览器会显示原生上下文菜单,因此通常需要调用 event.preventDefault() 来阻止默认行为。浏览器菜单事件参考见菜单事件

jsx 复制代码
const onNodeContextMenu = useCallback((event, node) => {
  event.preventDefault();
  console.log('Context menu for node:', node.id);
  // 显示自定义上下文菜单
}, []);

(六) 一些使用说明

1.1 性能优化

由于 ReactFlow 的事件处理程序会在每次交互时触发,使用 useCallback 来包装处理函数可以避免不必要的重新渲染。

jsx 复制代码
const onNodeClick = useCallback((event, node) => {
  // 处理逻辑
}, [dependencies]);

Reactflow 中一些方法例如

会有重渲染的问题, 如果需要获取当前工作流画布的缩放比例, 可以考虑一些替代性方法封装成 hooks 使用。如下给出替代方案

ini 复制代码
// 只订阅 zoom 变化
const zoom = useStore(store => store.transform[2]);

1.2 结合 useNodesState 管理节点状态

对于需要根据事件更新节点状态的场景,可以使用 useNodesState 钩子来简化状态管理。

jsx 复制代码
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);

const onNodeClick = useCallback((event, node) => {
  setNodes(nds => nds.map(n => {
    if (n.id === node.id) {
      return {
        ...n,
        data: { ...n.data, clicked: true }
      };
    }
    return n;
  }));
}, [setNodes]);

React Flow 作为基于 React 的现代化工作流编排工具,通过其声明式渲染协议和可扩展的节点系统,已成为百宝箱、扣子等主流工作流平台的核心画布引擎, 服务于智能体的搭建。 后续会从渲染协议继续介绍到一般性消费协议,聊一聊如何把画布渲染的数据转化成服务能够消费的数据,供大模型或者其他插件等等技能消费,以最终实现智能体的编排输出。

相关推荐
赛博丁真Damon26 分钟前
【VSCode插件】【p2p网络】为了硬写一个和MCP交互的日程表插件(Cursor/Trae),我学习了去中心化的libp2p
前端·cursor·trae
江城开朗的豌豆36 分钟前
Vue的keep-alive魔法:让你的组件"假死"也能满血复活!
前端·javascript·vue.js
BillKu1 小时前
Vue3 + TypeScript 中 let data: any[] = [] 与 let data = [] 的区别
前端·javascript·typescript
GIS之路1 小时前
OpenLayers 调整标注样式
前端
爱吃肉的小鹿1 小时前
Vue 动态处理多个作用域插槽与透传机制深度解析
前端
GIS之路1 小时前
OpenLayers 要素标注
前端
前端付豪1 小时前
美团 Flink 实时路况计算平台全链路架构揭秘
前端·后端·架构
sincere_iu1 小时前
#前端重铸之路 Day7 🔥🔥🔥🔥🔥🔥🔥🔥
前端·面试
设计师也学前端1 小时前
SVG数据可视化组件基础教程7:自定义柱状图
前端·svg
我想说一句1 小时前
当JavaScript的new操作符开始内卷:手写实现背后的奇妙冒险
前端·javascript