React Flow 作为基于 React 的现代化工作流编排工具,通过其声明式渲染协议和可扩展的节点系统,已成为百宝箱、扣子等主流工作流平台的核心画布引擎, 服务于智能体的搭建。
一、React Flow 简介
React Flow 是一个用于构建基于节点的应用程序的库。官方文档 reactflow.dev/learn可以实现自定义节点类型和边,它还视口控件等等组件。
- 易于使用:已经具备了许多开箱即用的功能。拖动节点、缩放和平移、选择多个节点和边缘以及添加/删除边都是内置功能。可定制:支持自定义节点类型和边类型。由于自定义节点只是 React 组件,因此你可以实现任何需要的功能,不会被锁定在内置的节点类型中。 自定义边可让你在节点边添加标签、控件和定制逻辑。快速渲染:只渲染发生变化的节点,并确保只显示视口中的节点。内置插件:提供了一些开箱即用的插件,如
<Background />
可实现一些基本的自定义背景图案;<MiniMap />
可在屏幕一角显示小版本的图形;<Controls />
可添加缩放、居中和锁定视口的控件;<Panel />
可让你轻松将内容放置在视口的顶部;<NodeToolbar />
可让你呈现连接到节点的工具栏;<NodeResizer />
可让你轻松为节点添加调整大小的功能。二、使用步骤
(一)安装
yarn add @xyflow/reactnpm install @xyflow/react
(二)创建第一个 Flow
reactflow
软件包导出 <ReactFlow />
组件作为默认导出。加上一些节点和边,就可以开始工作了。
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' } },];// 初始化边 // 边存在唯一 IDconst 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 自定义节点
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 注册节点
const nodeTypes = { textUpdater: TextUpdaterNode}; function Flow() { ... return ( <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} ... /> );}
1.3 消费自定义节点
const nodes = [ { id: 'node-1', type: 'textUpdater', position: { x: 0, y: 0 }, data: { value: 123 }, },];
批量设置节点setNodes
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 自定义边
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 边注册
// 2. 定义边类型const edgeTypes = { custom: CustomEdge,};// 3. 在ReactFlow中使用function Flow() { return ( <div style={{ width: '100%', height: '500px' }}> <ReactFlow edgeTypes={edgeTypes} nodes={[...]} edges={[...]} /> </div> );}
批量设置边 setEdges
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
: 被点击的边对象(包含 id
、source
、target
等属性)- 显示边的详细信息(如弹窗)选中边并高亮关联节点
const onEdgeClick = (event, edge) => { console.log('Clicked edge:', edge); // 示例:显示边属性弹窗 setSelectedEdge(edge);};
1.2 onEdgeDoubleClick
- 触发时机:用户双击边时触发。参数:同
onEdgeClick
典型用途:- 快速编辑边属性删除边(需配合确认逻辑)
const onEdgeDoubleClick = (event, edge) => { if (confirm('确认删除此边?')) { setEdges(eds => eds.filter(e => e.id !== edge.id)); }};
1.3 onEdgeMouseEnter
/ onEdgeMouseLeave
- 触发时机:鼠标悬停/离开边时触发。参数:同
onEdgeClick
典型用途:- 动态修改边样式(如颜色、虚线)显示悬停提示(Tooltip)
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[]
典型用途:- 清理与边关联的数据记录删除操作日志
const onEdgesDelete = (deletedEdges) => { console.log('Deleted edges:', deletedEdges); // 同步更新后端数据};
1.5 onEdgesChange
- 触发时机:边状态变化时触发(如选中、取消选中)。参数:边变更数组
EdgeChange[]
典型用途:- 受控模式下同步边状态与
useEdgesState
钩子配合使用const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);<ReactFlow onEdgesChange={onEdgesChange} />
2.1 onConnect
- 触发时机:用户成功连接两个节点时触发。参数:连接对象
Connection
(包含 source
、target
、sourceHandle
等)典型用途:- 将连接转换为边(需配合
addEdge
工具)验证连接合法性(如禁止循环连接)const onConnect = (connection) => { const newEdge = { ...connection, id: `edge-${Date.now()}` }; setEdges(eds => addEdge(newEdge, eds));};// setEdges 边设置方法
2.2 onReconnect
- 触发时机:用户拖动边的端点重新连接到其他节点时触发。参数:
oldEdge
: 原边对象newConnection
: 新连接对象- 更新边数据源/目标检查连接有效性(如端口类型匹配)
const onReconnect = (oldEdge, newConnection) => { setEdges(eds => eds.map(edge => edge.id === oldEdge.id ? { ...edge, ...newConnection } : edge ));};
3.1 isValidConnection
- 触发时机:用户尝试建立连接时实时验证。参数:连接对象
Connection
返回:boolean
(true
表示允许连接)典型用途:- 禁止自连接限制特定类型节点的连接
const isValidConnection = (connection) => { return connection.source !== connection.target; // 禁止自连接};
3.2 onEdgeUpdateStart
/ onEdgeUpdateEnd
- 触发时机:开始/结束拖动边端点时触发。参数:
event
: 鼠标事件edge
: 被拖动的边handleType
: "source" 或 "target"- 显示拖动提示记录操作状态
const onEdgeUpdateStart = (event, edge, handleType) => { console.log(`开始拖动边 ${edge.id} 的 ${handleType} 端点`);};
完整示例代码
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} /> );}
总结
- 交互控制:通过
onEdgeClick
、onEdgeMouseEnter
等实现边的高亮与提示。数据管理:onEdgesChange
和 useEdgesState
配合管理边状态。连接验证:isValidConnection
确保连接符合业务规则。高级功能:onReconnect
支持动态调整边端点。更多细节可参考 React Flow 官方文档。
(五) 节点事件详解
核心节点事件概览
ReactFlow 提供了一系列节点相关的事件处理程序,允许开发者对用户的交互行为做出响应。以下是主要的节点事件类型及其触发时机:
- onNodeClick - 当用户点击节点时触发onNodeDoubleClick - 当用户双击节点时触发onNodeDragStart - 当用户开始拖动节点时触发onNodeDrag - 当用户拖动节点过程中持续触发onNodeDragStop - 当用户停止拖动节点时触发onNodeMouseEnter - 当鼠标进入节点区域时触发onNodeMouseMove - 当鼠标在节点区域内移动时触发onNodeMouseLeave - 当鼠标离开节点区域时触发onNodeContextMenu - 当用户在节点上右键点击时触发onNodesDelete - 当节点被删除时触发
1.1 点击相关事件
onNodeClick 是最常用的节点事件之一,它允许开发者在用户点击节点时执行自定义逻辑。这个事件接收两个参数:事件对象和被点击的节点对象。
const onNodeClick = useCallback((event, node) => { console.log('Node clicked:', node.id, node.data); // 可以在这里更新节点状态或执行其他操作}, []);
onNodeDoubleClick 类似于单击事件,但只在快速连续点击两次时触发。这在需要区分单击和双击操作的场景中非常有用。
1.2 拖拽相关事件
ReactFlow 提供了完整的拖拽生命周期事件,使开发者能够精确控制节点的拖拽行为:
- onNodeDragStart:拖拽开始时触发,适合用于初始化拖拽状态或记录原始位置onNodeDrag:拖拽过程中持续触发,可用于实时更新相关UI或执行碰撞检测onNodeDragStop:拖拽结束时触发,适合用于提交最终位置或执行验证
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:鼠标离开节点区域时触发,适合用于清除悬停状态
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()
来阻止默认行为。浏览器菜单事件参考见菜单事件
const onNodeContextMenu = useCallback((event, node) => { event.preventDefault(); console.log('Context menu for node:', node.id); // 显示自定义上下文菜单}, []);
(六) 一些使用说明
1.1 性能优化
由于 ReactFlow 的事件处理程序会在每次交互时触发,使用 useCallback
来包装处理函数可以避免不必要的重新渲染。
const onNodeClick = useCallback((event, node) => { // 处理逻辑}, [dependencies]);
Reactflow 中一些方法例如
会有重渲染的问题, 如果需要获取当前工作流画布的缩放比例, 可以考虑一些替代性方法封装成 hooks 使用。如下给出替代方案
// 只订阅 zoom 变化const zoom = useStore(store => store.transform[2]);
1.2 结合 useNodesState 管理节点状态
对于需要根据事件更新节点状态的场景,可以使用 useNodesState
钩子来简化状态管理。
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 的现代化工作流编排工具,通过其声明式渲染协议和可扩展的节点系统,已成为百宝箱、扣子等主流工作流平台的核心画布引擎, 服务于智能体的搭建。 后续会从渲染协议继续介绍到一般性消费协议,聊一聊如何把画布渲染的数据转化成服务能够消费的数据,供大模型或者其他插件等等技能消费,以最终实现智能体的编排输出。