Skip to Content
教程高级使用计算流程

计算流程

🌐 Computing Flows

在本指南中,我们假设你已经了解了 React Flow 的核心概念以及如何实现自定义节点

通常在使用 React Flow 时,开发者会在 React Flow 之外处理他们的数据,将其发送到其他地方,比如服务器或数据库。相反,在本指南中,我们将向你展示如何直接在 React Flow 内部计算数据流。你可以将其用于基于连接数据更新节点,或者构建完全在浏览器内部运行的应用。

🌐 Usually with React Flow, developers handle their data outside of React Flow by sending it somewhere else, like on a server or a database. Instead, in this guide we’ll show you how to compute data flows directly inside of React Flow. You can use this for updating a node based on connected data, or for building an app that runs entirely inside the browser.

我们要构建什么?

🌐 What are we going to build?

在本指南结束时,你将构建一个交互式流程图,该流程图从三个单独的数字输入字段(红色、绿色和蓝色)生成一种颜色,并确定在该背景颜色上白色或黑色文本是否更易读。

🌐 By the end of this guide, you will build an interactive flow graph that generates a color out of three separate number input fields (red, green and blue), and determines whether white or black text would be more readable on that background color.

创建自定义节点

🌐 Creating custom nodes

让我们先创建一个自定义输入节点(NumberInput.js),并添加三个实例。我们将使用一个受控的 <input type="number" />,并在 onChange 事件处理程序中将其限制为 0 到 255 之间的整数。

🌐 Let’s start by creating a custom input node (NumberInput.js) and add three instances of it. We will be using a controlled <input type="number" /> and limit it to integer numbers between 0 - 255 inside the onChange event handler.

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'; 来使文本颜色始终可读。

🌐 Next, we’ll add a new custom node (ColorPreview.js) with one target handle for each color channel and a background that displays the resulting color. We can use mix-blend-mode: 'difference'; to make the text color always readable.

每当你在单个节点上有多个相同类型的句柄时,不要忘记给每一个分配一个独立的 id!

同时,我们也可以在此时将从输入节点到颜色节点的边添加到我们的 initialEdges 数组中。

计算数据

🌐 Computing data

我们如何将数据从输入节点传递到颜色节点?这是一个涉及两个步骤的过程,需要为此目的创建的两个钩子:

🌐 How do we get the data from the input nodes to the color node? This is a two step process that involves two hooks created for this exact purpose:

  1. 在节点的 data 对象中存储每个数字输入值,借助 updateNodeData 回调。
  2. 使用 useNodeConnections 查找哪些节点已连接,然后使用 useNodesData 接收来自已连接节点的数据。

第1步:将值写入数据对象

🌐 Step 1: Writing values to the data object

首先,让我们在 initialNodes 数组中的 data 对象内为输入节点添加一些初始值,并将它们用作输入节点的初始状态。然后,我们将从 [/api-reference/hooks/use-react-flow] 的 [useReactFlow] hook 中获取函数 updateNodeData,并在输入变化时使用它来更新该节点的 data 对象为新值。

🌐 First let’s add some initial values for the input nodes inside the data object in our initialNodes array and use them as an initial state for the input nodes. Then we’ll grab the function updateNodeData from the useReactFlow hook and use it to update the data object of the node with a new value whenever the input changes.

默认情况下,你传递给updateNodeData的数据将会与旧的数据对象合并。这使得执行部分更新更容易,并且可以在你忘记添加{...data}时保护你。你可以将{ replace: true }作为一个选项传递,以替换该对象。

在处理输入字段时,你不想直接将节点的 data 对象用作 UI 状态。

更新数据对象存在延迟,光标可能会不稳定地跳动,从而导致不想要的输入。

步骤 2:从已连接的节点获取数据

🌐 Step 2: Getting data from connected nodes

我们首先通过 useNodeConnections 钩子确定每个节点的所有连接,然后获取第一个已连接节点的 updateNodeData 数据。

🌐 We start by determining all connections for each node with the useNodeConnections hook and then fetching the data for the first connected node with updateNodeData.

请注意,每个句柄可以连接多个节点,并且你可能希望在你的应用中限制单个句柄的连接数量。请查看 连接限制示例 以了解如何操作。

就这样! 试着改变输入值,看看颜色实时变化。

改进代码

🌐 Improving the code

先获取连接,然后再为每个句柄单独获取数据可能看起来有些尴尬。对于像这样的多句柄节点,你应考虑创建一个自定义句柄组件,以隔离连接状态和节点数据绑定。我们可以在行内创建一个。

🌐 It might seem awkward to get the connections first, and then the data separately for each handle. For nodes with multiple handles like these, you should consider creating a custom handle component that isolates connection states and node data binding. We can create one inline.

ColorPreview.js
// {...} function CustomHandle({ id, label, onChange }) { const connections = useNodeConnections({ handleType: 'target', handleId: 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> ); }

我们可以将颜色提升为本地状态并像这样声明每个句柄:

🌐 We can promote color to local state and declare each handle like this:

ColorPreview.js
// {...} 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;

变得更加复杂

🌐 Getting more complex

现在我们有一个如何通过 React Flow 传输数据的简单示例。如果我们想做一些更复杂的事情,比如在传输过程中变换数据呢?或者甚至走不同的路径呢?我们也可以做到!

🌐 Now we have a simple example of how to pipe data through React Flow. What if we want to do something more complex, like transforming the data along the way? Or even take different paths? We can do that too!

继续流程

🌐 Continuing the flow

让我们扩展我们的流程。首先在颜色节点添加一个输出 <Handle type="source" position={Position.Right} />,并删除本地组件状态。

🌐 Let’s extend our flow. Start by adding an output <Handle type="source" position={Position.Right} /> to the color node and remove the local component state.

因为这个节点上没有输入字段,所以我们根本不需要保留本地状态。我们可以直接读取并更新节点的data对象。

接下来,我们添加一个新的节点(Lightness.js),它接收一个颜色对象并判断它是浅色还是深色。我们可以使用相对亮度公式  luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b 来计算颜色的感知亮度(0为最暗,255为最亮)。我们可以假设大于等于128的都是浅色。

🌐 Next, we add a new node (Lightness.js) that takes in a color object and determines if it is either a light or dark color. We can use the relative luminance formula  luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b to calculate the perceived brightness of a color (0 being the darkest and 255 being the brightest). We can assume everything >= 128 is a light color.

条件分支

🌐 Conditional branching

如果我们想根据感知的亮度在流程中走不同的路径怎么办?让我们的亮度节点有两个源句柄 lightdark,并根据源句柄 ID 分离节点 data 对象。如果你有多个源句柄,需要区分每个源句柄的数据,这是必要的。

🌐 What if we would like to take a different path in our flow based on the perceived lightness? Let’s give our lightness node two source handles light and dark and separate the node data object by source handle IDs. This is needed if you have multiple source handles to distinguish between each source handle’s data.

但是,“走不同的路由”是什么意思呢?一种解决方案是假设连接到目标句柄的 nullundefined 数据被认为是一个“停止”。在我们的例子中,如果颜色较浅,我们可以将传入的颜色写入 data.values.light,如果颜色较深,则写入 data.values.dark,并将相应的另一个值设置为 null

🌐 But what does it mean to “take a different route”? One solution would be to assume that null or undefined data hooked up to a target handle is considered a “stop”. In our case we can write the incoming color into data.values.light if it’s a light color and into data.values.dark if it’s a dark color and set the respective other value to null.

别忘了添加 flex-direction: column;align-items: end; 来重新定位句柄标签。

🌐 Don’t forget to add flex-direction: column; and align-items: end; to reposition the handle labels.

酷!现在我们只需要最后一个节点来看看它是否真的有效……我们可以创建一个自定义调试节点(Log.js)来显示连接的数据,然后就完成了!

🌐 Cool! Now we only need a last node to see if it actually works… We can create a custom debugging node (Log.js) that displays the hooked up data, and we’re done!

摘要

🌐 Summary

你已经学会了如何在流程中移动数据并在过程中进行转换。你所需要做的就是

🌐 You have learned how to move data through the flow and transform it along the way. All you need to do is

  1. 在节点的 data 对象中存储数据,并借助 updateNodeData 回调。
  2. 使用useNodeConnections查找哪些节点已连接,然后使用useNodesData从已连接的节点接收数据。

你可以通过将未定义的传入数据解释为“停止”来实现分支。顺便说一句,大多数也具有分支的流程图通常会将节点的触发与实际连接到节点的数据分开。虚幻引擎的蓝图就是一个很好的例子。

🌐 You can implement branching for example by interpreting incoming data that is undefined as a “stop”. As a side note, most flow graphs that also have a branching usually separate the triggering of nodes from the actual data hooked up to the nodes. Unreal Engines Blueprints are a good example for this.

在你离开之前最后提醒一点:你应该找到一种一致的方式来构建所有节点数据,而不是像我们刚才那样混合不同的想法。这意味着,例如,如果你开始按句柄ID来拆分数据,你就应该对所有节点都这样做,无论它们是否有多个句柄。能够在整个流程中对数据结构做出假设会让工作轻松得多。

Last updated on