React Flow 实战(二)—— 拖拽添加节点

react

上一篇 《React Flow 实战》介绍了自定义节点等基本操作,接下来就该撸一个真正的流程图了

 

一、ReactFlowProvider

React Flow 提供了两个 Hooks 来处理画布数据:

import { 

useStoreState,

useStoreActions

} from 'react-flow-renderer';

通常情况下可以直接使用它们来获取 nodes、edges

但如果页面上同时存在多个 ReactFlow,或者需要在 ReactFlow 外部操作画布数据,就需要使用 ReactFlowProvider 将整个画布包起来

于是整个流程图的入口文件 index.jsx 是这样的:

// index.jsx

import React, { useState } from 'react';

import { ReactFlowProvider } from 'react-flow-renderer';

import Sider from './Sider';

import Graph from './Graph';

import Toolbar from './Toolbar';

import flowStyles from './index.module.less';

export default function FlowPage() {

// 画布实例

const [reactFlowInstance, setReactFlowInstance] = useState(null);

return (

<div className={flowStyles.container}>

<ReactFlowProvider>

{/* 顶部工具栏 */}

<Toolbar instance={reactFlowInstance} />

<div className={flowStyles.main}>

{/* 侧边栏,展示可拖拽的节点 */}

<Sider />

{/* 画布,处理核心逻辑 */}

<Graph

instance={reactFlowInstance}

setInstance={setReactFlowInstance}

/>

</div>

</ReactFlowProvider>

</div>

);

}

这里创建了 reactFlowInstance 这个状态,用来保存 ReactFlow 创建后的实例

这个实例会在 Graph 中设置,但会在 Graph 和 Toolbar 中使用,所以将该状态提升到 index.js 中管理

但这种将 state 和 setState 都传给子组件的方式并不好,最好是使用 useReducer 加以改造,或者引入状态管理节制


整体的目录结构如下

  

二、拖拽添加节点

简单的拖拽添加节点,可以通过原生 API draggable 实现

在 Sider 中触发节点的 onDragStart 事件,然后在 Graph 中通过 ReactFlow onDrop 来接收

// Sider.jsx

import React from 'react';

import classnames from 'classnames';

import { useStoreState } from 'react-flow-renderer';

import flowStyles from '../index.module.less';

// 可用节点

const allowedNodes = [

{

name: 'Input Node',

className: flowStyles.inputNode,

type: 'input',

},

{

name: 'Relation Node',

className: flowStyles.relationNode,

type: 'relation', // 这是自定义节点类型

},

{

name: 'Output Node',

className: flowStyles.outputNode,

type: 'output',

},

];

export default function FlowSider() {

// 获取画布上的节点

const nodes = useStoreState((store) => store.nodes);

const onDragStart = (evt, nodeType) => {

// 记录被拖拽的节点类型

evt.dataTransfer.setData('application/reactflow', nodeType);

evt.dataTransfer.effectAllowed = 'move';

};

return (

<div className={flowStyles.sider}>

<div className={flowStyles.nodes}>

{allowedNodes.map((x, i) => (

<div

key={`${x.type}-${i}`}

className={classnames([flowStyles.siderNode, x.className])}

onDragStart={e => onDragStart(e, x.type)}

draggable

>

{x.name}

</div>

))}

</div>

<div className={flowStyles.print}>

<div className={flowStyles.printLine}>

节点数量:{ nodes?.length || '-' }

</div>

<ul className={flowStyles.printList}>

{

nodes.map((x) => (

<li key={x.id} className={flowStyles.printItem}>

<span className={flowStyles.printItemTitle}>{x.data.label}</span>

<span className={flowStyles.printItemTips}>({x.type})</span>

</li>

))

}

</ul>

</div>

</div>

);

}

上面还通过 useStoreState 拿到了画布上的节点信息 nodes,该 nodes 基于 Redux 管理,无需手动更新


在 Graph 中,首先需要通过 onLoad 回调得到 ReactFlow 实例

接着处理 onDragOver 事件,更新 dropEffect,和 effectAllowed 保持一致

然后在 onDrop 事件处理函数中,通过 getBoundingClientRect 获取画布容器的坐标信息

但坐标信息需要通过 ReactFlow 实例提供的 project 方法处理为 ReactFlow 坐标系

最后组装节点信息,更新 elements 即可 

// Graph/index.jsx

import React, { useState, useRef } from 'react';

import ReactFlow, { Controls } from 'react-flow-renderer';

import RelationNode from '../Node/relationNode';

import flowStyles from '../index.module.less';

function getHash(len) {

let length = Number(len) || 8;

const arr =

'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split('');

const al = arr.length;

let chars = '';

while (length--) {

chars += arr[parseInt(Math.random() * al, 10)];

}

return chars;

}

export default function FlowGraph(props) {

const { setInstance, instance } = props;

// 画布的 DOM 容器,用于计算节点坐标

const graphWrapper = useRef(null);

// 节点、连线 都通过 elements 来维护

const [elements, setElements] = useState(props.elements || []);

// 自定义节点

const nodeTypes = {

relation: RelationNode,

};

// 画布加载完毕,保存当前画布实例

const onLoad = (instance) => setInstance(instance);

const onDrop = (event) => {

event.preventDefault();

const reactFlowBounds = graphWrapper.current.getBoundingClientRect();

// 获取节点类型

const type = event.dataTransfer.getData('application/reactflow');

// 使用 project 将像素坐标转换为内部 ReactFlow 坐标系

const position = instance.project({

x: event.clientX - reactFlowBounds.left,

y: event.clientY - reactFlowBounds.top,

});

const newNode = {

id: getHash(),

type,

position,

// 传入节点 data

data: { label: `${type} node` },

};

setElements((els) => els.concat(newNode));

};const onDragOver = (event) => {

event.preventDefault();

event.dataTransfer.dropEffect = 'move';

};

return (

<div className={flowStyles.graph} ref={graphWrapper}>

<ReactFlow

elements={elements}

nodeTypes={nodeTypes}

onLoad={onLoad}

onDrop={onDrop}

onDragOver={onDragOver}

>

<Controls />

</ReactFlow>

</div>

);

}

完成以上逻辑,就能够从侧边栏拖拽节点添加到画布上了

// 可以先删除以上有关自定义节点 RelationNode 的代码,试试拖拽功能

但目前的节点只是展示出来了,暂时不能连线,或者更新节点数据,后面逐步完善

三、连线

在画布上连线的时候,会触发 ReactFlow onConnect 事件,并提供连线信息

然后通过 addEdge 来添加连线,这个方法接收两个参数 edgeParams 和 elements,最后返回全新的 elements

// Graph/index.jsx

import ReactFlow, { addEdge } from 'react-flow-renderer';

// ...

export default function FlowGraph(props) {

// ...

// 连线

const onConnect = params => setElements(els => addEdge(params, els));

return (

<ReactFlow

elements={elements}

onConnect={onConnect}

// other...

/>

);

}

如果需要设置连线类型,或者设置其他连线的信息,都可以通过 addEdge 的第一个参数来设置

从节点出口拉出的线,在连接到节点入口前,默认展示的是 bezier 类型的线

如果需要自定义连接中的线的样式,可以使用 connectionLineComponent,具体可以参考官方示例

另外,还可以通过 onEdgeUpdate 来更改连线的起点或终点,参考官方示例

四、获取画布数据

在最开始的 index.jsx 中维护了一份 ReactFlow 的画布实例 reactFlowInstance,并传给了 Graph 和 Toolbar

通过 reactFlowInstance 就可以很方便的获取画布数据

// Toolbar.jsx

import React, { useCallback } from 'react';

import classnames from 'classnames';

import flowStyles from '../index.module.less';

export default function Toolbar({ instance }) {

// 保存

const handleSave = useCallback(() => {

console.log('toObject', instance.toObject());

}, [instance]);

return (

<div className={flowStyles.toolbar}>

<button

className={classnames([flowStyles.button, flowStyles.primaryBtn])}

onClick={handleSave}

>

保存

</button>

</div>

);

}

上面使用的是 Instance.toObject,拿到的是画布的全量数据,如果只需要 elements 可以使用 Instance.getElements

完整的实例方法可以参考官方文档

除了通过实例获取画布数据,还可以使用 useStoreState 

import ReactFlow, { useStoreState } from 'react-flow-renderer';

const NodesDebugger = () => {

const nodes = useStoreState((state) => state.nodes);

const edges = useStoreState((state) => state.edges);

console.log('nodes', nodes);

console.log('edges', edges);

return null;

};

const Flow = () => (

<ReactFlow elements={elements}>

<NodesDebugger />

</ReactFlow>

);

但这样获取的 nodes 会携带一些画布信息

具体使用哪种方式,可以根据实际的业务场景来取舍 


实际项目中的流程图,通常都会在节点甚至连线上配置各种数据

我们可以通过 elements 中各个元素的 data 来维护,但这真的合理吗?

elements 保存了节点和连线的位置、样式信息,用于 ReactFlow 绘制流程图,和业务数据并无关联

所以我建议以 map 的形式单独维护业务数据,可以通过节点或连线的 id 快速查找

具体的实现方案有很多,下一篇文章将介绍基于 React Context 的流程图数据管理方案

// 文章还在施工中,有兴趣可以先看下项目 flow-demo-app

以上是 React Flow 实战(二)—— 拖拽添加节点 的全部内容, 来源链接: utcz.com/z/384030.html

回到顶部