上一篇 《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 的流程圖數據管理方案