前面兩篇關於 React Flow 的文章已經介紹了如何繪制流程圖
而實際項目中,流程圖上的每一個節點,甚至每一條連線都需要維護一份獨立的業務數據
這篇文章將介紹通過 React.context 來管理流程圖數據的實際應用
項目結構:
.
├── Graph
│ └── index.jsx
├── Sider
│ └── index.jsx
├── Toolbar
│ └── index.jsx
├── components
│ ├── Edge
│ │ ├── LinkEdge.jsx
│ │ └── PopoverCard.jsx
│ ├── Modal
│ │ ├── RelationNodeForm.jsx
│ │ └── index.jsx
│ └── Node
│ └── RelationNode.jsx
├── context
│ ├── actions.js
│ ├── index.js
│ └── reducer.js
├── flow.css
└── flow.jsx
結合項目代碼食用更香,倉庫地址:https://github.com/wisewrong/bolg-demo-app/tree/main/flow-demo-app
一、定義 state
代碼未敲,設計先行。在正式動工之前,先想清楚應該維護哪些數據
首先是 React Flow 的畫布實例 reactFlowInstance,它會在 Graph.jsx 中創建並使用
另外 Toolbar.jsx 中保存的時候也會用到 reactFlowInstance,所以可以將它放到 context 中維護
然后是 React Flow 的節點/連線信息 elements,以及每個節點/連線對應的配置信息,它們可以放到 elements 中,通過每個元素的 data 來維護
但我更傾向於將業務數據拆開,用 elements 維護坐標等畫布信息,另外創建一個 Map 對象 flowData 來維護業務數據
配置節點/連線業務數據的表單通常是放在 Modal 或 Drawer 里,它們肯定會放到畫布外 難道還能放到節點里?,但通過節點/連線來觸發
所以還需要另外維護一個 modalConfig,來控制 Modal 的顯示/隱藏,以及傳入 Modal 的節點數據
所以最終的 state 是這樣的:
const initState = { // 畫布實例
reactFlowInstance: null, // 節點數據、連線數據
elements: [], // 畫布數據
flowData: new Map(), // 彈窗信息
modalConfig: { visible: false, nodeType: '', nodeId: '', }, };
二、創建 context
管理整個畫布的狀態,自然就會用到 useReducer
為了便於維護,我將整個 context 拆為三部分:index.js、reducer.js、actions.js
其中 actions.js 用來管理 dispatch 的事件名稱:
// context/actions.js
export const SET_INSTANCE = 'set_instance'; export const SET_ELEMENTS = 'set_elements'; export const SET_FLOW_NODE = 'set_flow_node'; export const REMOVE_FLOW_NODE = 'remove_flow_node'; export const OPEN_MODAL = 'open_modal'; export const CLOSE_MODAL = 'close_modal';
reducer.js 管理具體的事件處理邏輯
// context/reducer.js
import * as Actions from "./actions"; // 保存畫布實例
const setInstance = (state, reactFlowInstance) => ({ ...state, reactFlowInstance, }); // 設置節點/連線數據
const setElements = (state, elements) => ({ ...state, elements: Array.isArray(elements) ? elements : [], }); // 保存節點配置信息
const setFlowNode = (state, node) => { // ...
}; // 刪除節點,同時刪除節點配置信息
const removeFlowNode = (state, node) => { // ...
}; const openModal = (state, node) => { // ...
} const closeModal = (state) => { // ...
} // 管理所有處理函數
const handlerMap = { [Actions.SET_INSTANCE]: setInstance, [Actions.SET_FLOW_NODE]: setFlowNode, [Actions.REMOVE_FLOW_NODE]: removeFlowNode, [Actions.OPEN_MODAL]: openModal, [Actions.CLOSE_MODAL]: closeModal, [Actions.SET_ELEMENTS]: setElements, }; const reducer = (state, action) => { const { type, payload } = action; const handler = handlerMap[type]; const res = typeof handler === "function" && handler(state, payload); return res || state; }; export default reducer;
最后 index.js 管理初始狀態,並導出相關產物
// context/index.js
import React, { createContext, useReducer } from 'react'; import reducer from './reducer'; import * as Actions from './actions'; const FlowContext = createContext(); const initState = { // 畫布實例
reactFlowInstance: null, // 節點數據、連線數據
elements: [], // 畫布數據
flowData: new Map(), // 彈窗信息
modalConfig: { visible: false, nodeType: '', nodeId: '', }, }; const FlowContextProvider = (props) => { const { children } = props; const [state, dispatch] = useReducer(reducer, initState); return ( <FlowContext.Provider value={{ state, dispatch }}> {children} </FlowContext.Provider> ); }; export { FlowContext, FlowContextProvider, Actions };
三、節點的添加與刪除
建立好狀態管理體系之后,就可以通過 Provider 使用了
// flow.jsx
import React from 'react'; import { ReactFlowProvider } from 'react-flow-renderer'; import Sider from './Sider'; import Graph from './Graph'; import Toolbar from './Toolbar'; import Modal from './components/Modal'; // 引入 Provider
import { FlowContextProvider } from './context'; import './flow.css'; export default function FlowPage() { return ( <div className="container">
<FlowContextProvider>
<ReactFlowProvider> {/* 頂部工具欄 */} <Toolbar />
<div className="main"> {/* 側邊欄,展示可拖拽的節點 */} <Sider /> {/* 畫布,處理核心邏輯 */} <Graph />
</div> {/* 彈窗,配置節點數據 */} <Modal />
</ReactFlowProvider>
</FlowContextProvider>
</div> ); }
上一篇文章《React Flow 實戰(二)—— 拖拽添加節點》已經介紹過拖放節點,這里就不再贅述拖拽的實現
在添加節點之后,需要通過 reducer 中的方法來更新數據
// Graph/index.jsx
import React, { useRef, useContext } from "react"; import ReactFlow, { addEdge, Controls } from "react-flow-renderer"; import { FlowContext, Actions } from "../context"; export default function FlowGraph(props) { const { state, dispatch } = useContext(FlowContext); const { elements, reactFlowInstance } = state; const setReactFlowInstance = (instance) => { dispatch({ type: Actions.SET_INSTANCE, payload: instance, }); }; const setElements = (els) => { dispatch({ type: Actions.SET_ELEMENTS, payload: els, }); }; // 畫布加載完畢,保存當前畫布實例
const onLoad = (instance) => setReactFlowInstance(instance); // 連線
const onConnect = (params) => setElements( addEdge( { ...params, type: "link", }, elements ) ); // 拖拽完成后放置節點
const onDrop = (event) => { event.preventDefault(); const newNode = { // ...
}; dispatch({ type: Actions.SET_FLOW_NODE, payload: { id: newNode.id, ...newNode.data, }, }); setElements(elements.concat(newNode)); }; // ...
}
同時在 reducer.js 中完善相應的邏輯,通過節點 id 維護節點數據
// context/reducer.js // 保存節點配置信息
const setFlowNode = (state, node) => { const nodeId = node?.id; if (!nodeId) return state; state.flowData.set(nodeId, node); return state; }; // ...
由於 elements 和 flowData 已經解耦,所以如需更新節點數據,直接使用 setFlowNode 更新 flowData 即可,不需要操作 elements
而如果是刪除節點,可以通過 ReactFlow 提供的 removeElements 方法來快速處理 elements
// context/reducer.js
import { removeElements } from "react-flow-renderer"; // 刪除節點,同時刪除節點配置信息
const removeFlowNode = (state, node) => { const { id } = node; const { flowData } = state; const res = { ...state }; if (flowData.get(id)) { flowData.delete(id); res.elements = removeElements([node], state.elements); } return res; }; // ...
節點數據的增刪改就完成了,只要保證在所有需要展示節點信息的地方(畫布節點、彈窗表單、連線彈窗)都通過 flowData 獲取,維護起來就會很輕松
四、彈窗表單
最后再聊一聊關於彈窗表單的設計
一開始設計 state 的時候就提到過,整個畫布只有一個彈窗,為此還專門維護了一份 modalConfig
彈窗可以只有一個,但不同類型的節點對應的表單卻各有不同,這時候就需要創建不同的表單組件,通過節點類型來切換
// Modal/index.jsx
import React, { useContext, useRef } from "react"; import { Modal } from "antd"; import RelationNodeForm from "./RelationNodeForm"; import { FlowContext, Actions } from "../../context"; // 通過節點類型來切換對應的表單組件
const componentsMap = { relation: RelationNodeForm, }; export default function FlowModal() { const formRef = useRef(); const { state, dispatch } = useContext(FlowContext); const { modalConfig } = state; const handleOk = () => { // 組件內部需要暴露一個 submit 方法
formRef.current.submit().then(() => { dispatch({ type: Actions.CLOSE_MODAL }); }); }; const handleCancel = () => dispatch({ type: Actions.CLOSE_MODAL }); const Component = componentsMap[modalConfig.nodeType]; return ( <Modal title="編輯節點" visible={modalConfig.visible} onOk={handleOk} onCancel={handleCancel}> {Component && <Component ref={formRef} />} </Modal> ); }
但不同的表單組件,最后都是通過彈窗 footer 上的“確定”按鈕來提交,而提交表單的邏輯卻有可能不同
我這里的做法是,在表單組件內部暴露一個 submit 方法,通過彈窗的 onOk 回調觸發
// Modal/RelationNodeForm.jsx
import React, { useContext, useEffect, useImperativeHandle } from "react"; import { Input, Form } from "antd"; import { FlowContext, Actions } from "../../context"; function RelationNodeForm(props, ref) { const { state, dispatch } = useContext(FlowContext); const { flowData, modalConfig } = state; const [form] = Form.useForm(); const initialValues = flowData.get(modalConfig.nodeId) || {}; useImperativeHandle(ref, () => ({ // 將 submit 方法暴露給父組件
submit: () => { return form .validateFields() .then((values) => { dispatch({ type: Actions.SET_FLOW_NODE, payload: { id: modalConfig.nodeId, ...values, }, }); }) .catch((err) => { return false; }); }, })); useEffect(() => { form.resetFields(); }, [modalConfig.nodeId, form]); return ( <Form form={form} initialValues={initialValues}> {/* Form.Item */} </Form> ); } export default React.forwardRef(RelationNodeForm);
關於 React Flow 的實戰就到這里了,本文介紹的是狀態管理,所以很多業務代碼就沒有貼出來
有需要的可以看下 GitHub 上的代碼,倉庫地址在本文的開頭已經貼出來了
總的來說 React Flow 用起來還是挺方便,配合良好的狀態管理體系,應該能適用於大部分的流程圖需求
如果以后遇到了相當復雜的場景,我會再分享出來~