本文为《React Agent:从零开始构建 AI 智能体》专栏系列文章。 专栏地址:https://blog.csdn.net/suiyingy/category_12933485.html。项目地址:https://gitee.com/fgai/react-agent(含完整代码示例与实战源)。完整介绍:https://blog.csdn.net/suiyingy/article/details/146983582。
1 鼠标事件
React Flow 为节点提供了丰富的鼠标事件,如onClick、onDoubleClick、onMouseEnter、onMouseLeave等。这些事件可以用于实现节点的各种交互功能。onClick事件可以用于打开节点的详细信息面板,onMouseEnter事件可以用于显示节点的提示信息。
const InfoNode = ({ data }) => {
return (
<div
className="react-flow__node-default"
onClick={() => alert('This is the info of the node')}
onMouseEnter={() => console.log('Mouse entered the node')}
onMouseLeave={() => console.log('Mouse left the node')}
>
{data.label}
</div>
);
};
alert和console.log都是 JavaScript 中常用的工具,在调试程序时发挥着不同作用。alert是浏览器提供的一个全局函数,它会弹出一个包含指定消息的对话框,此对话框会暂停当前程序的执行,直到用户点击“确定”关闭它。在调试中,它可用于快速确认变量的值或某些代码块是否被执行。不过,由于它会中断程序,若频繁使用会影响用户体验,并且只能展示简单的文本信息。
console.log则是向浏览器的控制台输出信息。控制台是开发者调试代码的重要工具,它可以显示各种类型的数据,如字符串、数字、对象、数组等。使用console.log不会中断程序执行,所以能在程序运行过程中持续输出信息,方便开发者观察变量的变化和程序的执行流程。
若要在浏览器中查看console.log输出的信息,不同浏览器的操作方式略有不同,但基本步骤一致。首先,在浏览器中打开包含该代码的网页;然后,打开开发者工具,常见的方法是在网页上右键点击,选择 “检查” 或者 “审查元素”,也可以使用快捷键(通常为 F12);打开开发者工具后,切换到 “控制台”(Console)面板,在这里就能看到console.log输出的信息。运行上面程序后会有如下输出。
图1 鼠标事件
我们也可对连接点 Handle 设置事件,下面程序可双击修改 Handle 名称。
import React, { useCallback, useContext, useState } from 'react';
import { ReactFlow, Handle, useNodesState, useEdgesState, addEdge } from 'reactflow';
import 'reactflow/dist/style.css';
import { FiDatabase, FiCloud } from 'react-icons/fi';
import { toast, Toaster } from 'react-hot-toast';
// 创建上下文用于节点更新
const NodeUpdateContext = React.createContext();
const CustomNode = ({ id, data, selected }) => {
const setNodes = useContext(NodeUpdateContext);
const [editingHandle, setEditingHandle] = useState(null);
const [tempName, setTempName] = useState('');
const handleNameChange = (handleId, newName) => {
setNodes(nds => nds.map(node =>
node.id === id ? {
...node,
data: {
...node.data,
handleNames: {
...node.data.handleNames,
[handleId]: newName
}
}
} : node
));
};
const startEdit = (handleId) => {
setEditingHandle(handleId);
setTempName(data.handleNames?.[handleId] || '');
};
const confirmEdit = () => {
if (editingHandle) {
handleNameChange(editingHandle, tempName);
setEditingHandle(null);
}
};
const renderHandle = (localId, position, type) => {
const fullId = `${id}-${localId}`;
const isEditing = editingHandle === fullId;
const displayName = data.handleNames?.[fullId] || '';
return (
<div className={`handle-group handle-${position}`}>
{isEditing && (
<input
type="text"
value={tempName}
onChange={(e) => setTempName(e.target.value)}
onBlur={confirmEdit}
onKeyPress={(e) => e.key === 'Enter' && confirmEdit()}
autoFocus
className="handle-input"
/>
)}
<Handle
id={fullId}
type={type}
position={position}
className={`!bg-${type === 'target' ? 'teal' : 'purple'}-500`}
onDoubleClick={() => startEdit(fullId)}
title={displayName}
/>
</div>
);
};
return (
<div className={`custom-node ${selected ? 'selected' : ''}`}>
{renderHandle('target-top', 'top', 'target')}
<div className="node-header">
<FiCloud className="node-icon" />
<h3 className="node-title">{data.label}</h3>
</div>
<div className="node-body">
<FiDatabase className="node-icon" />
<span className="node-info">{data.content}</span>
</div>
{renderHandle('source-bottom', 'bottom', 'source')}
{renderHandle('source-right', 'right', 'source')}
</div>
);
};
// 初始节点配置(保持其他内容不变,增加handleNames字段)
const initialNodes = [
{
id: '1',
position: { x: 0, y: 0 },
data: {
label: '开始节点',
content: '输入数据源',
handleNames: {
'1-target-top': '输入',
'1-source-bottom': '主输出',
'1-source-right': '备选输出'
}
},
type: 'custom',
},
{
id: '2',
position: { x: 200, y: 150 },
data: {
label: '处理节点',
content: '数据处理流程',
handleNames: {
'2-target-top': '输入',
'2-source-bottom': '结果输出',
'2-source-right': '日志输出'
}
},
type: 'custom',
},
];
const initialEdges = [{
id: 'e1-2',
source: '1',
target: '2',
animated: true,
style: { stroke: '#94a3b8' },
}];
const nodeTypes = {
custom: CustomNode,
};
// 样式增加连接点标签相关样式
const nodeStyle = `
.custom-node {
position: relative;
background: linear-gradient(145deg, #ffffff, #f1f5f9);
border-radius: 8px;
border: 2px solid #cbd5e1;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 16px;
min-width: 200px;
transition: all 0.2s ease;
}
.custom-node.selected {
border-color: #6366f1;
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.2);
}
.custom-node:hover {
transform: translateY(-2px);
}
.node-header {
display: flex;
align-items: center;
margin-bottom: 12px;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 8px;
}
.node-title {
margin: 0;
font-size: 1.1rem;
color: #1e293b;
margin-left: 8px;
}
.node-body {
display: flex;
align-items: center;
color: #64748b;
}
.node-icon {
font-size: 1.2rem;
margin-right: 8px;
color: #6366f1;
}
.node-info {
font-size: 0.9rem;
}
.react-flow__handle {
width: 24px;
height: 14px;
border-radius: 3px;
border: none;
cursor: pointer;
transition: background-color 0.2s;
}
.react-flow__handle:hover {
filter: brightness(1.2);
}
/* 按位置调整具体坐标 */
.react-flow__handle[data-position="top"] {
top: -7px !important;
left: 50% !important;
transform: translateX(-50%) !important;
}
.react-flow__handle[data-position="bottom"] {
bottom: -7px !important;
left: 50% !important;
transform: translateX(-50%) !important;
}
.react-flow__handle[data-position="right"] {
right: -12px !important;
top: 50% !important;
transform: translateY(-50%) !important;
}
.handle-group {
z-index: 10;
}
.handle-input {
position: absolute;
width: 80px;
padding: 2px 4px;
font-size: 0.8rem;
border: 1px solid #6366f1;
border-radius: 4px;
background: white;
z-index: 100;
}
.handle-top .handle-input {
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
}
.handle-bottom .handle-input {
top: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
}
.handle-right .handle-input {
left: calc(100% + 8px);
top: 50%;
transform: translateY(-50%);
}
`;
export default function App() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
return (
<NodeUpdateContext.Provider value={setNodes}>
<div style={{ height: '100vh', background: '#f8fafc' }}>
<style>{nodeStyle}</style>
<Toaster position="top-right" />
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={useCallback((conn) =>
setEdges(eds => addEdge({
...conn,
animated: true,
style: conn.sourceHandle?.endsWith('-right')
? { stroke: '#ec4899' }
: { stroke: '#94a3b8' }
}, eds)), [setEdges])}
nodeTypes={nodeTypes}
isValidConnection={useCallback((conn) =>
conn.source !== conn.target && !edges.some(e => e.target === conn.target), [edges])}
fitView
defaultEdgeOptions={{
type: 'smoothstep',
animated: true,
style: { strokeWidth: 2 }
}}
/>
</div>
</NodeUpdateContext.Provider>
);
}
2 键盘事件
在某些场景下,需要通过键盘来操作节点,React Flow 支持键盘事件。可以监听onKeyDown、onKeyUp等事件,实现节点的键盘导航、快捷键操作等功能。例如,通过按下特定的快捷键来删除节点:
const InfoNode = ({ data }) => {
const handleKeyDown = (event) => {
if (event.key === 'Delete') {
alert('节点将被删除');
}
};
return (
<div
className="react-flow__node-default"
tabIndex={0}
onKeyDown={handleKeyDown}
>
{data.label}
</div>
);
};
3 拖动与连接事件
与节点的拖动和连接相关的事件也非常重要。onDragStart、onDrag、onDragEnd事件可以用于处理节点拖动过程中的逻辑,如更新节点的位置、限制拖动范围等。onConnect事件则在节点之间建立连接时触发,可以用于验证连接的合法性、更新数据等。需要注意,直接在自定义节点组件上使用 draggable 和原生的 onDragStart 等事件会与 React Flow 的拖拽行为冲突。如果开发者希望为节点添加额外的拖动逻辑,可以利用 React Flow 自身提供的事件(例如 onNodeDragStart、onNodeDrag 和 onNodeDragStop)来实现。
import React, { useCallback } from 'react';
import {
ReactFlow,
Handle,
useNodesState,
useEdgesState,
addEdge,
} from 'reactflow';
import 'reactflow/dist/style.css';
// 自定义节点组件
const InfoNode = ({ data }) => {
return (
<div className="react-flow__node-default">
{data.label}
</div>
);
};
const nodeTypes = {
infoNode: InfoNode,
};
// 节点配置
const infoNode = {
id: 'info-node-1',
type: 'infoNode',
data: { label: 'Info Node' },
position: { x: 250, y: 100 },
};
const initialNodes = [infoNode];
const initialEdges = [];
export default function App() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
);
const handleNodeDragStart = (event, node) => {
console.log('Node drag started:', node);
};
const handleNodeDrag = (event, node) => {
console.log('Node dragging:', node);
};
const handleNodeDragStop = (event, node) => {
console.log('Node drag stopped:', node);
};
return (
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeDragStart={handleNodeDragStart}
onNodeDrag={handleNodeDrag}
onNodeDragStop={handleNodeDragStop}
fitView
/>
</div>
);
}
4 创建自定义事件
除了使用内置事件,开发者还可以根据项目需求创建自定义事件。在 React 里,自定义事件属于开发者自行定义的事件,它可以让组件之间更好地进行通信和交互。它能够在特定条件达成时触发特定的行为,从而增强组件的灵活性与可复用性。而 useEffect 是 React 提供的一个 Hook,其主要作用是处理副作用操作,像数据获取、订阅、DOM 操作等。它会在组件渲染之后执行,并且可以依据依赖项数组的变化来决定是否重新执行。通过结合自定义事件和 useEffect,我们能够在组件的生命周期内灵活地触发和处理自定义事件,从而实现更为复杂的交互逻辑。
const InfoNode = ({ data }) => {
const nodeRef = useRef(null);
useEffect(() => {
const nodeEl = nodeRef.current;
if (!nodeEl) return;
const handleClick = () => alert('This is the info of the node');
const handleMouseEnter = () => console.log('Mouse entered the node');
const handleMouseLeave = () => console.log('Mouse left the node');
nodeEl.addEventListener('click', handleClick);
nodeEl.addEventListener('mouseenter', handleMouseEnter);
nodeEl.addEventListener('mouseleave', handleMouseLeave);
// 清理
return () => {
nodeEl.removeEventListener('click', handleClick);
nodeEl.removeEventListener('mouseenter', handleMouseEnter);
nodeEl.removeEventListener('mouseleave', handleMouseLeave);
};
}, []); // 空依赖数组,确保只在挂载和卸载时执行
return (
<div
ref={nodeRef}
className="react-flow__node-default"
>
{data.label}
</div>
);
};
5 事件冒泡与捕获
开发者在处理多个节点的事件时需要了解事件冒泡和捕获机制。事件冒泡是指事件从最内层的元素开始触发,然后逐级向上传播到外层元素;事件捕获则相反,从最外层元素开始,逐级向内层元素传播。合理利用事件冒泡和捕获能够实现更高效的事件处理。例如,在一个包含多个节点的区域中,我们可以在父元素上监听事件,通过事件属性来判断具体是哪个节点触发了事件,从而减少事件处理函数的重复定义。
import React, { useCallback } from 'react';
import {
ReactFlow,
Handle,
useNodesState,
useEdgesState,
addEdge,
} from 'reactflow';
import 'reactflow/dist/style.css';
// 自定义节点组件
const InfoNode = ({ data }) => {
const { onNodeClick, onNodeMouseEnter, onNodeMouseLeave, label } = data;
return (
<div
className="react-flow__node-default"
onClick={() => onNodeClick(label)}
onMouseEnter={() => onNodeMouseEnter(label)}
onMouseLeave={() => onNodeMouseLeave(label)}
>
{label}
</div>
);
};
const nodeTypes = {
infoNode: InfoNode,
};
// 节点配置
const infoNode1 = {
id: 'info-node-1',
type: 'infoNode',
data: { label: 'Info Node 1' },
position: { x: 250, y: 100 },
};
const infoNode2 = {
id: 'info-node-2',
type: 'infoNode',
data: { label: 'Info Node 2' },
position: { x: 500, y: 100 },
};
const initialNodes = [infoNode1, infoNode2];
const initialEdges = [];
export default function App() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onNodeClick = (label) => {
alert(`This is the info of ${label}`);
};
const onNodeMouseEnter = (label) => {
console.log(`Mouse entered the ${label}`);
};
const onNodeMouseLeave = (label) => {
console.log(`Mouse left the ${label}`);
};
const newNodes = nodes.map((node) => ({
...node,
data: {
...node.data,
onNodeClick,
onNodeMouseEnter,
onNodeMouseLeave,
},
}));
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
);
return (
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow
nodes={newNodes}
edges={edges}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
/>
</div>
);
}
立即关注获取最新动态
点击订阅《React Agent 开发专栏》,每周获取智能体开发深度教程。项目代码持续更新至React Agent 开源仓库,欢迎 Star 获取实时更新通知!FGAI 人工智能平台:FGAI 人工智能平台