上一篇 《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