计算流
通常,使用 React Flow 时,开发人员会在 React Flow 之外处理他们的数据,并将数据发送到其他地方,例如服务器或数据库。在本指南中,我们将向您展示如何在 React Flow 内部直接计算数据流。您可以使用它根据连接的数据更新节点,或者构建一个完全在浏览器内部运行的应用程序。
我们要构建什么?
在本指南结束时,您将构建一个交互式流图,它从三个单独的数字输入字段(红色、绿色和蓝色)生成颜色,并确定在该背景颜色上白色或黑色文本哪个更易读。
创建自定义节点
让我们从创建一个自定义输入节点(NumberInput.js
)并添加三个实例开始。我们将使用受控的 <input type="number" />
并将其限制为 onChange
事件处理程序中的 0-255 之间的整数。
import { useCallback, useState } from 'react';
import { Handle, Position } from '@xyflow/react';
function NumberInput({ id, data }) {
const [number, setNumber] = useState(0);
const onChange = useCallback((evt) => {
const cappedNumber = Math.round(
Math.min(255, Math.max(0, evt.target.value)),
);
setNumber(cappedNumber);
}, []);
return (
<div className="number-input">
<div>{data.label}</div>
<input
id={`number-${id}`}
name="number"
type="number"
min="0"
max="255"
onChange={onChange}
className="nodrag"
value={number}
/>
<Handle type="source" position={Position.Right} />
</div>
);
}
export default NumberInput;
接下来,我们将添加一个新的自定义节点(ColorPreview.js
),每个颜色通道有一个目标句柄,以及显示结果颜色的背景。我们可以使用 mix-blend-mode: 'difference';
使文本颜色始终易读。
当您在单个节点上有多个相同类型的句柄时,请记住为每个句柄提供单独的 ID!
在我们进行时,让我们也向 initialEdges
数组中添加从输入节点到颜色节点的边。
计算数据
我们如何将数据从输入节点获取到颜色节点?这是一个两步过程,涉及为此目的创建的两个钩子。
- 使用
updateNodeData
回调,将每个数字输入值存储在节点的data
对象中。 - 使用
useHandleConnections
找出哪些节点已连接,然后使用useNodesData
从连接的节点接收数据。
步骤 1:将值写入数据对象
首先,让我们在 initialNodes
数组中为输入节点的 data
对象添加一些初始值,并将其用作输入节点的初始状态。然后,我们将从 useReactFlow
钩子中获取函数 updateNodeData
,并使用它在输入发生变化时使用新值更新节点的 data
对象。
默认情况下,您传递给 updateNodeData
的数据将与旧数据对象合并。这使得部分更新变得更容易,并防止您在忘记添加 {...data}
时出错。您可以传递 { replace: true }
作为选项,以改为替换对象。
在处理输入字段时,您不希望将节点的 data
对象直接用作 UI 状态。
更新数据对象存在延迟,光标可能会不规则地跳动,从而导致意外输入。
步骤 2:从连接的节点获取数据
我们首先使用 useHandleConnections
钩子确定每个句柄的所有连接,然后使用 updateNodeData
获取第一个连接节点的数据。
请注意,每个句柄可以连接多个节点,您可能希望在应用程序中将单个句柄的连接数量限制为一个。查看 连接限制示例,了解如何做到这一点。
就是这样! 尝试更改输入值,并查看颜色实时更改。
改进代码
获取连接,然后分别获取每个句柄的数据,这似乎有些奇怪。对于像这样的具有多个句柄的节点,您应该考虑创建一个自定义句柄组件,该组件隔离连接状态和节点数据绑定。我们可以内联创建一个。
// {...}
function CustomHandle({ id, label, onChange }) {
const connections = useHandleConnections({
type: 'target',
id,
});
const nodeData = useNodesData(connections?.[0].source);
useEffect(() => {
onChange(nodeData?.data ? nodeData.data.value : 0);
}, [nodeData]);
return (
<div>
<Handle
type="target"
position={Position.Left}
id={id}
className="handle"
/>
<label htmlFor="red" className="label">
{label}
</label>
</div>
);
}
我们可以将颜色提升到局部状态并像这样声明每个句柄
// {...}
function ColorPreview() {
const [color, setColor] = useState({ r: 0, g: 0, b: 0 });
return (
<div
className="node"
style={{
background: `rgb(${color.r}, ${color.g}, ${color.b})`,
}}
>
<CustomHandle
id="red"
label="R"
onChange={(value) => setColor((c) => ({ ...c, r: value }))}
/>
<CustomHandle
id="green"
label="G"
onChange={(value) => setColor((c) => ({ ...c, g: value }))}
/>
<CustomHandle
id="blue"
label="B"
onChange={(value) => setColor((c) => ({ ...c, b: value }))}
/>
</div>
);
}
export default ColorPreview;
变得更加复杂
现在我们有一个简单的例子,展示了如何通过 React Flow 传输数据。如果我们想做更复杂的事情,比如在传输过程中转换数据,甚至走不同的路径呢?我们也可以做到!
继续流动
让我们扩展我们的流程。首先,在颜色节点添加一个输出 <Handle type="source" position={Position.Right} />
并删除本地组件状态。
由于该节点上没有输入字段,因此我们完全不需要保留本地状态。我们可以直接读取和更新节点的 data
对象。
接下来,我们添加一个新的节点(Lightness.js
),该节点接受一个颜色对象并确定它到底是浅色还是深色。我们可以使用 相对亮度公式 luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b
来计算颜色的感知亮度(0 为最暗,255 为最亮)。我们可以假设所有 >= 128 的颜色都是浅色。
条件分支
如果我们想根据感知亮度走不同的路径呢?让我们为我们的亮度节点添加两个源句柄 light
和 dark
,并将节点 data
对象按源句柄 ID 分开。如果您有多个源句柄,则需要这样做以区分每个源句柄的数据。
但“走不同的路线”是什么意思?一种解决方案是假设连接到目标句柄的 null
或 undefined
数据被视为“停止”。在我们的案例中,如果颜色是浅色,我们可以将传入的颜色写入 data.values.light
,如果颜色是深色,则写入 data.values.dark
,并将相应的其他值设置为 null
。
不要忘记添加 flex-direction: column;
和 align-items: end;
来重新定位句柄标签。
酷!现在我们只需要最后一个节点来查看它是否真的有效……我们可以创建一个自定义调试节点(Log.js
),它将显示连接的数据,然后就完成了!
总结
您已经学习了如何在流程中移动数据并沿途进行转换。您只需要
- 使用
updateNodeData
回调将数据存储在节点的data
对象中。 - 使用
useHandleConnections
找出哪些节点已连接,然后使用useNodesData
接收来自已连接节点的数据。
您可以通过将未定义的传入数据解释为“停止”来实现分支。顺便说一下,大多数也具有分支的流程图通常将节点的触发与实际连接到节点的数据分开。虚幻引擎的蓝图就是一个很好的例子。
在您离开之前,最后一点:您应该找到一种一致的方式来构建所有节点数据,而不是像我们现在这样混合想法。这意味着,例如,如果您开始通过句柄 ID 拆分数据,那么您应该对所有节点都这样做,无论它们是否有多个句柄。能够对整个流程中数据的结构进行假设将使生活变得轻松得多。