• React Flow 实战(三)—— 使用 React.context 管理流程图数据


    前面两篇关于 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 用起来还是挺方便,配合良好的状态管理体系,应该能适用于大部分的流程图需求

    如果以后遇到了相当复杂的场景,我会再分享出来~

     

  • 相关阅读:
    Android 2.2 r1 API 中文文档系列(11) —— RadioButton
    Android API 中文 (15) —— GridView
    Android 中文 API (16) —— AnalogClock
    Android2.2 API 中文文档系列(7) —— ImageButton
    Android2.2 API 中文文档系列(6) —— ImageView
    Android 2.2 r1 API 中文文档系列(12) —— Button
    Android2.2 API 中文文档系列(8) —— QuickContactBadge
    [Android1.5]TextView跑马灯效果
    [Android1.5]ActivityManager: [1] Killed am start n
    Android API 中文(14) —— ViewStub
  • 原文地址:https://www.cnblogs.com/wisewrong/p/15638354.html
Copyright © 2020-2023  润新知