• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

这节我们来学下用 React Flow 来画流程图。

有同学会问,这有什么应用么?

有很多。

现在很多 AI 工具都支持通过流程图来画工作流,然后执行:

比如 Coze 的:

2024-08-29 00.13.25.gif

或者 Defi 的:

image.png

在 Defi 的前端代码可以看到用的 reactflow

image.png

工作流是 AI 工具的一个卖点,并不是所有的 AI 工具都支持:

image.png

能作为业务卖点的功能,这技术就很有价值了。

而且低代码编辑器的事件处理也可以用流程图来做逻辑编排:

2024-08-29 00.23.58.gif

2024-08-29 00.24.58.gif

然后执行这个流程。

同样,不是所有的低代码平台都支持逻辑编排,这也是一个可以作为业务卖点的功能。

所以说,学习 React Flow 是很有价值的。

下面我们新建个项目来学习下:

npx create-vite react-flow-test

image.png

进入项目,安装 reactflow 的包:

npm install
npm install --save @xyflow/react

把 main.tsx 里的样式和 StrictMode 去掉。

image.png

然后改一下 App.tsx

import { ReactFlow } from "@xyflow/react";
import "@xyflow/react/dist/style.css";

const initialNodes = [
    { id: "1", position: { x: 0, y: 0 }, data: { label: "1" } },
    { id: "2", position: { x: 0, y: 100 }, data: { label: "2" } },
];
const initialEdges = [{ id: "e1-2", source: "1", target: "2" }];

export default function App() {
    return (
        <div
            style={{
                width: "800px",
                height: "500px",
                border: "1px solid #000",
                margin: "50px auto",
            }}>
            <ReactFlow nodes={initialNodes} edges={initialEdges} />
        </div>
    );
}

其实 React Flow 也很简单,就是指定 node(节点) 和 edge(边) 就好了。

比如我们指定了 2 个节点:

id 是节点 id、position 是在画布中的坐标,data 是传递给节点组件的参数。

指定了一条边来连接 id 为 1 和 id 为 2 的节点。

image.png

指定了这两部分,就是一个流程图了:

image.png

跑起来看下效果:

npm run dev

image.png

2024-08-29 00.45.41.gif

可以看到,流程图绘制出来了,可以放缩画布,但不能拖动流程图。

这是因为没处理拖动的事件,加一下就好了:

image.png

因为只有 node 和 edge,所以也就只有三种事件: node 变化、edge 变化、node 和 node 连接,也就是 onNodesChange、onEdgesChange、onConnect

onNodesChange 和 onEdgesChange 直接用 reactflow 提供的就好。

而 onConnect 就是调用 addEdge 新加一条边,然后调用 setEdges 更新。

import {
    addEdge,
    Connection,
    OnConnect,
    ReactFlow,
    useEdgesState,
    useNodesState,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";

const initialNodes = [
    { id: "1", position: { x: 0, y: 0 }, data: { label: "1" } },
    { id: "2", position: { x: 0, y: 100 }, data: { label: "2" } },
];
const initialEdges = [{ id: "e1-2", source: "1", target: "2" }];

export default function App() {
    const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
    const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

    const onConnect = (params: Connection) => {
        setEdges((eds) => addEdge(params, eds));
    };

    return (
        <div
            style={{
                width: "800px",
                height: "500px",
                border: "1px solid #000",
                margin: "50px auto",
            }}>
            <ReactFlow
                nodes={nodes}
                edges={edges}
                onNodesChange={onNodesChange}
                onEdgesChange={onEdgesChange}
                onConnect={onConnect}
            />
        </div>
    );
}

试一下:

2024-08-29 00.58.01.gif

现在节点就可以拖拽了,并且可以删除节点、删除边、创建新的连接。

ractflow 还内置了一些工具。

image.png

分别是控制条、缩略图、背景:

image.png

当然,用点做背景不太明显,你可以换成线:

image.png

<Background variant={BackgroundVariant.Lines} />

image.png

你可以用鼠标滚轮放缩画布,也可以用控制条来控制:

2024-08-29 01.06.32.gif

或者可以在 MiniMap 上加上 zoomable 参数:

image.png

这样就可以通过缩略图放缩画布了:

2024-08-29 01.09.45.gif

回到流程图本身:

像这些节点明显都是自己绘制的:

image.png

我们也实现下:

import {
    addEdge,
    Background,
    BackgroundVariant,
    Connection,
    Controls,
    Handle,
    MiniMap,
    OnConnect,
    Position,
    ReactFlow,
    useEdgesState,
    useNodesState,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";

const initialNodes = [
    { id: "1", position: { x: 0, y: 0 }, type: "red", data: { label: "1" } },
    {
        id: "2",
        position: { x: 200, y: 300 },
        type: "blue",
        data: { label: "2" },
    },
];
const initialEdges = [{ id: "e1-2", source: "1", target: "2" }];

interface NodePorps {
    data: {
        label: string;
    };
}
function RedNode({ data }: NodePorps) {
    return (
        <div
            style={{
                background: "red",
                width: "100px",
                height: "100px",
                textAlign: "center",
            }}>
            <Handle type="source" position={Position.Right} />
            <Handle type="target" position={Position.Bottom} />

            <div>{data.label}</div>
        </div>
    );
}

function BlueNode({ data }: NodePorps) {
    return (
        <div
            style={{
                background: "blue",
                width: "50px",
                height: "50px",
                textAlign: "center",
                color: "#fff",
            }}>
            <Handle type="source" position={Position.Bottom} />
            <Handle type="target" position={Position.Top} />

            <div>{data.label}</div>
        </div>
    );
}

export default function App() {
    const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
    const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

    const onConnect = (params: Connection) => {
        setEdges((eds) => addEdge(params, eds));
    };

    return (
        <div
            style={{
                width: "800px",
                height: "500px",
                border: "1px solid #000",
                margin: "50px auto",
            }}>
            <ReactFlow
                nodes={nodes}
                edges={edges}
                onNodesChange={onNodesChange}
                onEdgesChange={onEdgesChange}
                onConnect={onConnect}
                nodeTypes={{
                    red: RedNode,
                    blue: BlueNode,
                }}>
                <Controls />
                <MiniMap zoomable />
                <Background variant={BackgroundVariant.Lines} />
            </ReactFlow>
        </div>
    );
}

我们增加了两个组件 RedNode 和 BlueNode:

image.png

它的 props 里的 data 就是 node 里的 data 属性。

那渲染 node 的时候如何对应到不同的组件呢?

根据 nodeTypes 的映射:

image.png

我们指定了 type 为 red 就渲染 RedNode,这样渲染流程图的时候就会用对应的组件来渲染。

看下效果:

image.png

可以看到,渲染的节点变成了我们自己的组件。

你可以绘制更多东西,比如这种:

image.png

而且节点上有一个黑点可以用来连接别的节点:

2024-08-29 01.33.09.gif

这个黑点叫做 Handle:

image.png

在自定义节点里,用 Handle 来指定黑点出现的位置。

还要指定 type,可以连线从 source 的 Handle 到 source 为 target 的 Handle,不能反过来。

除了 node 外, edge 自然也是可以自定义的。

import {
    addEdge,
    Background,
    BackgroundVariant,
    BaseEdge,
    Connection,
    Controls,
    EdgeLabelRenderer,
    EdgeProps,
    getBezierPath,
    getStraightPath,
    Handle,
    MiniMap,
    OnConnect,
    Position,
    ReactFlow,
    useEdgesState,
    useNodesState,
    useReactFlow,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";

const initialNodes = [
    { id: "1", position: { x: 0, y: 0 }, type: "red", data: { label: "1" } },
    {
        id: "2",
        position: { x: 200, y: 300 },
        type: "blue",
        data: { label: "2" },
    },
];
const initialEdges = [{ id: "e1-2", source: "1", target: "2", type: "custom" }];

interface NodePorps {
    data: {
        label: string;
    };
}
function RedNode({ data }: NodePorps) {
    return (
        <div
            style={{
                background: "red",
                width: "100px",
                height: "100px",
                textAlign: "center",
            }}>
            <Handle type="source" position={Position.Right} />
            <Handle type="target" position={Position.Bottom} />

            <div>{data.label}</div>
        </div>
    );
}

function BlueNode({ data }: NodePorps) {
    return (
        <div
            style={{
                background: "blue",
                width: "50px",
                height: "50px",
                textAlign: "center",
                color: "#fff",
            }}>
            <Handle type="source" position={Position.Bottom} />
            <Handle type="target" position={Position.Top} />

            <div>{data.label}</div>
        </div>
    );
}

function CustomEdge({
    id,
    sourceX,
    sourceY,
    targetX,
    targetY,
    sourcePosition,
    targetPosition,
    style = {},
    markerEnd,
}: EdgeProps) {
    const { setEdges } = useReactFlow();

    const [edgePath, labelX, labelY] = getBezierPath({
        sourceX,
        sourceY,
        sourcePosition,
        targetX,
        targetY,
        targetPosition,
    });

    const onEdgeClick = () => {
        setEdges((edges) => edges.filter((edge) => edge.id !== id));
    };

    return (
        <>
            <BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
            <EdgeLabelRenderer>
                <div
                    style={{
                        position: "absolute",
                        transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
                        fontSize: 12,
                        // EdgeLabelRenderer 里的组件默认不处理鼠标事件,如果要处理就要声明 pointerEvents: all
                        pointerEvents: "all",
                    }}>
                    <button onClick={onEdgeClick}>×</button>
                </div>
            </EdgeLabelRenderer>
        </>
    );
}

export default function App() {
    const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
    const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

    const onConnect = (params: Connection) => {
        setEdges((eds) => addEdge(params, eds));
    };

    return (
        <div
            style={{
                width: "800px",
                height: "500px",
                border: "1px solid #000",
                margin: "50px auto",
            }}>
            <ReactFlow
                nodes={nodes}
                edges={edges}
                onNodesChange={onNodesChange}
                onEdgesChange={onEdgesChange}
                onConnect={onConnect}
                nodeTypes={{
                    red: RedNode,
                    blue: BlueNode,
                }}
                edgeTypes={{
                    custom: CustomEdge,
                }}>
                <Controls />
                <MiniMap zoomable />
                <Background variant={BackgroundVariant.Lines} />
            </ReactFlow>
        </div>
    );
}

和 nodeTypes 自定义节点类似,在 edgeTypes 里配置自定义边就可以了:

image.png

绘制边稍微复杂一些:

image.png

主要是要根据传入的起始位置和目标位置,计算一条贝塞尔曲线,拿到 path 之后传入 BaseEdge 就可以绘制边了。

如果想绘制额外的内容,就放在 EdgeLabelRenderer 组件里。

这里 button 我们想加上点击事件,需要设置 pointerEvents: all,不然默认不响应鼠标事件。

点击的时候,删除这条边,也是调用 setEdeges,这个 API 通过 useReactFlow 的 hook 拿到。

看下效果:

2024-08-29 01.53.27.gif

绘制的边多了一个按钮,并且点击可以删除这条边。

有同学可能会说,如果我想绘制直线呢?

那更简单,只需要起始、结束点的坐标就能计算出路径:

image.png

const [edgePath, labelX, labelY] = getStraightPath({
    sourceX,
    sourceY,
    targetX,
    targetY,
});

现在绘制的就是直线了:

image.png

最后我们在右上角加一个按钮:

image.png

点击的时候添加一个节点:

<Panel position="top-right">
    <button
        onClick={() => {
            setNodes([
                ...nodes,
                {
                    id: Math.random().toString().slice(2, 6) + "",
                    type: "red",
                    position: { x: 0, y: 0 },
                    data: {
                        label: "光",
                    },
                },
            ]);
        }}>
        添加节点
    </button>
</Panel>

试下效果:

2024-08-29 02.06.12.gif

至此,reactflow 的常用功能我们就学会了。

案例代码上传了小册仓库

总结

这节我们学会了基于 React Flow 画流程图。

它有很多有价值的应用,比如 AI 工具的工作流编辑、低代码的逻辑编排等。

但它学起来并不难,核心就是 node(节点) 和 edge(边)。

对应三个事件: nodesChange(节点变化)、edgesChange(边变化)、connect(连接节点和节点)

每个节点可以通过 type 指定绘制用的组件,我们可以自定义节点的内容,通过 data 指定传给组件的参数。

然后通过 nodeTypes 来指定 type 和组件的映射。

同样,边也可以自定义,通过 edgeTypes 来指定映射。

在自定义 node 里,通过 Handle 来声明黑点位置。

在自定义 edge 里,通过 BaseEdge 来画线,通过 EdgeLabelRenderer 来画其他内容。

画边的时候计算贝塞尔曲线的路径、直线的路径用 reactflow 提供的 hook 就行。

此外,还可以通过 Controls、MiniMap、Background 来实现控制条、缩略图、背景等功能。通过 Panel 组件在画布上放置一些按钮之类的。

学完了这个案例,React Flow 就算入门了。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
79.低代码编辑器:项目总结
Next
81.ReactFlow振荡器调音:项目介绍