上一篇 《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 '../components/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) { // 画布的 DOM 容器,用于计算节点坐标 const graphWrapper = useRef(null); // 画布实例 const [reactFlowInstance, setReactFlowInstance] = useState(null); // 节点、连线 都通过 elements 来维护 const [elements, setElements] = useState(props.elements || []); // 自定义节点 const nodeTypes = { relation: RelationNode, }; // 画布加载完毕,保存当前画布实例 const onl oad = (instance) => setReactFlowInstance(instance); const onDrop = (event) => { event.preventDefault(); const reactFlowBounds = graphWrapper.current.getBoundingClientRect(); // 获取节点类型 const type = event.dataTransfer.getData('application/reactflow'); // 使用 project 将像素坐标转换为内部 ReactFlow 坐标系 const position = reactFlowInstance.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} onl oad={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 的流程图数据管理方案