zoukankan      html  css  js  c++  java
  • 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 用起来还是挺方便,配合良好的状态管理体系,应该能适用于大部分的流程图需求

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

     

  • 相关阅读:
    Vue 实现前进刷新,后退不刷新的效果
    chrome浏览器的跨域设置——包括版本49前后两种设置
    Promise.all和Promise.race区别,和使用场景
    滚动条默认最底部
    使用react进行父子组件传值
    java 数组基础学习(一维二维数组)
    react项目 使用echarts
    Python的hasattr() getattr() setattr() 函数使用方法详解
    【线性判别】Fisher线性判别(转)
    【semantic segmentation】Pyramid Scene Parsing Network(转)
  • 原文地址:https://www.cnblogs.com/wisewrong/p/15638354.html
Copyright © 2011-2022 走看看