曹祝林 發表於 2021-12-6 14:46:00

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

<p><span style="font-size: 14px">前面两篇关于 React Flow 的文章已经介绍了如何绘制流程图</span></p>
<p><span style="font-size: 14px">而实际项目中,流程图上的每一个节点,甚至每一条连线都需要维护一份独立的业务数据</span></p>
<p><span style="font-size: 14px">这篇文章将介绍通过 React.context 来管理流程图数据的实际应用</span></p>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 14px"><img src="https://img2020.cnblogs.com/blog/1059788/202112/1059788-20211206114150730-2145534077.gif" alt="" loading="lazy"></span></p>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 14px">项目结构:</span></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1); font-size: 14px">.
├── 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</span></pre>
</div>
<p><span style="font-size: 14px">结合项目代码食用更香,仓库地址:https://github.com/wisewrong/bolg-demo-app/tree/main/flow-demo-app</span></p>
<p>&nbsp;</p>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 16px"><strong>一、定义 state</strong></span></p>
<p><span style="font-size: 14px">代码未敲,设计先行。在正式动工之前,先想清楚应该维护哪些数据</span></p>
<p><span style="font-size: 14px">首先是 React Flow 的画布实例 <span style="color: rgba(128, 0, 0, 1)"><strong>reactFlowInstance</strong></span>,它会在<span style="color: rgba(0, 128, 128, 1)">&nbsp;Graph.jsx</span> 中创建并使用</span></p>
<p><span style="font-size: 14px">另外 <span style="color: rgba(0, 128, 128, 1)">Toolbar.jsx</span> 中保存的时候也会用到&nbsp;reactFlowInstance,所以可以将它放到 context 中维护</span></p>
<hr>
<p><span style="font-size: 14px">然后是 React Flow 的节点/连线信息 <span style="color: rgba(128, 0, 0, 1)"><strong>elements</strong></span>,以及每个节点/连线对应的配置信息,它们可以放到 elements 中,通过每个元素的 data 来维护</span></p>
<p><span style="font-size: 14px">但我更倾向于将业务数据拆开,用 elements 维护坐标等画布信息,另外创建一个 Map 对象 <strong><span style="color: rgba(128, 0, 0, 1)">flowData</span></strong> 来维护业务数据</span></p>
<hr>
<p><span style="font-size: 14px">配置节点/连线业务数据的表单通常是放在 Modal 或&nbsp;Drawer 里,它们肯定会放到画布外&nbsp;<span style="text-decoration: line-through"><span style="color: rgba(153, 153, 153, 1); text-decoration: line-through">难道还能放到节点里?</span></span><span style="color: rgba(153, 153, 153, 1)"><span style="color: rgba(0, 0, 0, 1)">,但通过节点/连线来触发</span></span></span></p>
<p><span style="font-size: 14px">所以还需要另外维护一个&nbsp;<span style="color: rgba(128, 0, 0, 1)"><strong>modalConfig</strong></span>,来控制 Modal 的显示/隐藏,以及传入 Modal 的节点数据</span></p>
<hr>
<p><span style="font-size: 14px">所以最终的 state 是这样的:</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px"><span style="color: rgba(0, 0, 255, 1)">const</span> initState =<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 画布实例</span>
reactFlowInstance: <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 节点数据、连线数据</span>
<span style="color: rgba(0, 0, 0, 1)">elements: [],
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 画布数据</span>
flowData: <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Map(),
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 弹窗信息</span>
<span style="color: rgba(0, 0, 0, 1)">modalConfig: {
    visible: </span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">,
    nodeType: </span><span style="color: rgba(128, 0, 0, 1)">''</span><span style="color: rgba(0, 0, 0, 1)">,
    nodeId: </span><span style="color: rgba(128, 0, 0, 1)">''</span><span style="color: rgba(0, 0, 0, 1)">,
},
};</span></span></pre>
</div>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 16px"><strong>二、创建 context</strong></span></p>
<p><span style="font-size: 14px">管理整个画布的状态,自然就会用到&nbsp;<span style="color: rgba(128, 0, 0, 1)"><strong>useReducer</strong></span></span></p>
<p><span style="font-size: 14px">为了便于维护,我将整个 context 拆为三部分:<span style="color: rgba(0, 128, 128, 1)">index.js、reducer.js、actions.js</span></span></p>
<p><span style="font-size: 14px">其中&nbsp;<span style="color: rgba(0, 0, 255, 1)"><strong>actions.js</strong> </span>用来管理&nbsp;dispatch 的事件名称:</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px"><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> context/actions.js</span>
<span style="color: rgba(0, 0, 0, 1)">
export </span><span style="color: rgba(0, 0, 255, 1)">const</span> SET_INSTANCE = <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">set_instance</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
export </span><span style="color: rgba(0, 0, 255, 1)">const</span> SET_ELEMENTS = <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">set_elements</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
export </span><span style="color: rgba(0, 0, 255, 1)">const</span> SET_FLOW_NODE = <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">set_flow_node</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
export </span><span style="color: rgba(0, 0, 255, 1)">const</span> REMOVE_FLOW_NODE = <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">remove_flow_node</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
export </span><span style="color: rgba(0, 0, 255, 1)">const</span> OPEN_MODAL = <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">open_modal</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
export </span><span style="color: rgba(0, 0, 255, 1)">const</span> CLOSE_MODAL = <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">close_modal</span><span style="color: rgba(128, 0, 0, 1)">'</span>;</span></pre>
</div>
<p><span style="font-size: 14px"><span style="color: rgba(0, 0, 255, 1)"><strong>reducer.js</strong></span> 管理具体的事件处理逻辑</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px"><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> context/reducer.js</span>
<span style="color: rgba(0, 0, 0, 1)">
import </span>* <span style="color: rgba(0, 0, 255, 1)">as</span> Actions <span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">./actions</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 保存画布实例</span>
<span style="color: rgba(0, 0, 255, 1)">const</span> setInstance = (state, reactFlowInstance) =&gt;<span style="color: rgba(0, 0, 0, 1)"> ({
...state,
reactFlowInstance,
});

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 设置节点/连线数据</span>
<span style="color: rgba(0, 0, 255, 1)">const</span> setElements = (state, elements) =&gt;<span style="color: rgba(0, 0, 0, 1)"> ({
...state,
elements: Array.isArray(elements) </span>?<span style="color: rgba(0, 0, 0, 1)"> elements : [],
});

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 保存节点配置信息</span>
<span style="color: rgba(0, 0, 255, 1)">const</span> setFlowNode = (state, node) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
<span style="color: rgba(0, 0, 0, 1)">};

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 删除节点,同时删除节点配置信息</span>
<span style="color: rgba(0, 0, 255, 1)">const</span> removeFlowNode = (state, node) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
<span style="color: rgba(0, 0, 0, 1)">};

</span><span style="color: rgba(0, 0, 255, 1)">const</span> openModal = (state, node) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
<span style="color: rgba(0, 0, 0, 1)">}

</span><span style="color: rgba(0, 0, 255, 1)">const</span> closeModal = (state) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
<span style="color: rgba(0, 0, 0, 1)">}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 管理所有处理函数</span>
<span style="color: rgba(0, 0, 255, 1)">const</span> handlerMap =<span style="color: rgba(0, 0, 0, 1)"> {
: setInstance,
: setFlowNode,
: removeFlowNode,
: openModal,
: closeModal,
: setElements,
};

</span><span style="color: rgba(0, 0, 255, 1)">const</span> reducer = (state, action) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">const</span> { type, payload } =<span style="color: rgba(0, 0, 0, 1)"> action;
</span><span style="color: rgba(0, 0, 255, 1)">const</span> handler =<span style="color: rgba(0, 0, 0, 1)"> handlerMap;
</span><span style="color: rgba(0, 0, 255, 1)">const</span> res = <span style="color: rgba(0, 0, 255, 1)">typeof</span> handler === <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">function</span><span style="color: rgba(128, 0, 0, 1)">"</span> &amp;&amp;<span style="color: rgba(0, 0, 0, 1)"> handler(state, payload);
</span><span style="color: rgba(0, 0, 255, 1)">return</span> res ||<span style="color: rgba(0, 0, 0, 1)"> state;
};

export </span><span style="color: rgba(0, 0, 255, 1)">default</span> reducer;</span></pre>
</div>
<p><span style="font-size: 14px">最后 <span style="color: rgba(0, 0, 255, 1)"><strong>index.js</strong></span> 管理初始状态,并导出相关产物</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px"><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> context/index.js</span>
<span style="color: rgba(0, 0, 0, 1)">
import React, { createContext, useReducer } </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">react</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
import reducer </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">./reducer</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
import </span>* <span style="color: rgba(0, 0, 255, 1)">as</span> Actions <span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">./actions</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;

</span><span style="color: rgba(0, 0, 255, 1)">const</span> FlowContext =<span style="color: rgba(0, 0, 0, 1)"> createContext();

</span><span style="color: rgba(0, 0, 255, 1)">const</span> initState =<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 画布实例</span>
reactFlowInstance: <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 节点数据、连线数据</span>
<span style="color: rgba(0, 0, 0, 1)">elements: [],
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 画布数据</span>
flowData: <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Map(),
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 弹窗信息</span>
<span style="color: rgba(0, 0, 0, 1)">modalConfig: {
    visible: </span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">,
    nodeType: </span><span style="color: rgba(128, 0, 0, 1)">''</span><span style="color: rgba(0, 0, 0, 1)">,
    nodeId: </span><span style="color: rgba(128, 0, 0, 1)">''</span><span style="color: rgba(0, 0, 0, 1)">,
},
};

</span><span style="color: rgba(0, 0, 255, 1)">const</span> FlowContextProvider = (props) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">const</span> { children } =<span style="color: rgba(0, 0, 0, 1)"> props;
</span><span style="color: rgba(0, 0, 255, 1)">const</span> =<span style="color: rgba(0, 0, 0, 1)"> useReducer(reducer, initState);
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
    </span>&lt;FlowContext.Provider value={{ state, dispatch }}&gt;<span style="color: rgba(0, 0, 0, 1)">
      {children}
    </span>&lt;/FlowContext.Provider&gt;<span style="color: rgba(0, 0, 0, 1)">
);
};

export { FlowContext, FlowContextProvider, Actions };</span></span></pre>
</div>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 16px"><strong>三、节点的添加与删除</strong></span></p>
<p><span style="font-size: 14px">建立好状态管理体系之后,就可以通过 Provider 使用了</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px"><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> flow.jsx</span>
<span style="color: rgba(0, 0, 0, 1)">
import React </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">react</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
import { ReactFlowProvider } </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">react-flow-renderer</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
import Sider </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">./Sider</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
import Graph </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">./Graph</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
import Toolbar </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">./Toolbar</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
import Modal </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">./components/Modal</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 引入 Provider</span>
import { FlowContextProvider } <span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">./context</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;

import </span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">./flow.css</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">;

export </span><span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)"> function FlowPage() {
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
    </span>&lt;div className=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">container</span><span style="color: rgba(128, 0, 0, 1)">"</span>&gt;
      &lt;FlowContextProvider&gt;
      &lt;ReactFlowProvider&gt;<span style="color: rgba(0, 0, 0, 1)">
          {</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> 顶部工具栏 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">}
          </span>&lt;Toolbar /&gt;
          &lt;div className=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">main</span><span style="color: rgba(128, 0, 0, 1)">"</span>&gt;<span style="color: rgba(0, 0, 0, 1)">
            {</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> 侧边栏,展示可拖拽的节点 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">}
            </span>&lt;Sider /&gt;<span style="color: rgba(0, 0, 0, 1)">
            {</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> 画布,处理核心逻辑 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">}
            </span>&lt;Graph /&gt;
          &lt;/div&gt;<span style="color: rgba(0, 0, 0, 1)">
          {</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> 弹窗,配置节点数据 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">}
          </span>&lt;Modal /&gt;
      &lt;/ReactFlowProvider&gt;
      &lt;/FlowContextProvider&gt;
    &lt;/div&gt;<span style="color: rgba(0, 0, 0, 1)">
);
}</span></span></pre>
</div>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 14px">上一篇文章《React Flow 实战(二)—— 拖拽添加节点》已经介绍过拖放节点,这里就不再赘述拖拽的实现</span></p>
<p><span style="font-size: 14px">在添加节点之后,需要通过 reducer 中的方法来更新数据</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px"><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Graph/index.jsx</span>
<span style="color: rgba(0, 0, 0, 1)">
import React, { useRef, useContext } </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">react</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
import ReactFlow, { addEdge, Controls } </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">react-flow-renderer</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
import { FlowContext, Actions } </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">../context</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;

export </span><span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)"> function FlowGraph(props) {
</span><span style="color: rgba(0, 0, 255, 1)">const</span> { state, dispatch } =<span style="color: rgba(0, 0, 0, 1)"> useContext(FlowContext);
</span><span style="color: rgba(0, 0, 255, 1)">const</span> { elements, reactFlowInstance } =<span style="color: rgba(0, 0, 0, 1)"> state;

</span><span style="color: rgba(0, 0, 255, 1)">const</span> setReactFlowInstance = (instance) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    dispatch({
      type: Actions.SET_INSTANCE,
      payload: instance,
    });
};

</span><span style="color: rgba(0, 0, 255, 1)">const</span> setElements = (els) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    dispatch({
      type: Actions.SET_ELEMENTS,
      payload: els,
    });
};

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 画布加载完毕,保存当前画布实例</span>
<span style="color: rgba(0, 0, 255, 1)">const</span> onLoad = (instance) =&gt;<span style="color: rgba(0, 0, 0, 1)"> setReactFlowInstance(instance);

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 连线</span>
<span style="color: rgba(0, 0, 255, 1)">const</span> onConnect = (<span style="color: rgba(0, 0, 255, 1)">params</span>) =&gt;<span style="color: rgba(0, 0, 0, 1)">
    setElements(
      addEdge(
      {
          ...</span><span style="color: rgba(0, 0, 255, 1)">params</span><span style="color: rgba(0, 0, 0, 1)">,
          type: </span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">link</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
      },
      elements
      )
    );

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 拖拽完成后放置节点</span>
<span style="color: rgba(0, 0, 255, 1)">const</span> onDrop = (<span style="color: rgba(0, 0, 255, 1)">event</span>) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    </span><span style="color: rgba(0, 0, 255, 1)">event</span><span style="color: rgba(0, 0, 0, 1)">.preventDefault();

    </span><span style="color: rgba(0, 0, 255, 1)">const</span> newNode =<span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
<span style="color: rgba(0, 0, 0, 1)">    };
    dispatch({
      type: Actions.SET_FLOW_NODE,
      payload: {
      id: newNode.id,
      ...newNode.data,
      },
    });
    setElements(elements.concat(newNode));
};

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
}</span></pre>
</div>
<p><span style="font-size: 14px">同时在 <span style="color: rgba(0, 128, 128, 1)">reducer.js</span> 中完善相应的逻辑,通过节点 id 维护节点数据</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px"><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> context/reducer.js

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 保存节点配置信息</span>
<span style="color: rgba(0, 0, 255, 1)">const</span> setFlowNode = (state, node) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">const</span> nodeId = node?<span style="color: rgba(0, 0, 0, 1)">.id;
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (!nodeId) <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> state;
state.flowData.</span><span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">(nodeId, node);
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> state;
};

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span></span></pre>
</div>
<p><span style="font-size: 14px">由于 elements 和 flowData 已经解耦,所以如需更新节点数据,直接使用 setFlowNode 更新 flowData 即可,不需要操作 elements</span></p>
<p><span style="font-size: 14px">而如果是删除节点,可以通过 ReactFlow 提供的&nbsp;<span style="color: rgba(128, 0, 0, 1)"><strong>removeElements</strong></span> 方法来快速处理 elements</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px"><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> context/reducer.js</span>
<span style="color: rgba(0, 0, 0, 1)">
import { removeElements } </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">react-flow-renderer</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 删除节点,同时删除节点配置信息</span>
<span style="color: rgba(0, 0, 255, 1)">const</span> removeFlowNode = (state, node) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">const</span> { id } =<span style="color: rgba(0, 0, 0, 1)"> node;
</span><span style="color: rgba(0, 0, 255, 1)">const</span> { flowData } =<span style="color: rgba(0, 0, 0, 1)"> state;
</span><span style="color: rgba(0, 0, 255, 1)">const</span> res =<span style="color: rgba(0, 0, 0, 1)"> { ...state };

</span><span style="color: rgba(0, 0, 255, 1)">if</span> (flowData.<span style="color: rgba(0, 0, 255, 1)">get</span><span style="color: rgba(0, 0, 0, 1)">(id)) {
    flowData.delete(id);
    res.elements </span>=<span style="color: rgba(0, 0, 0, 1)"> removeElements(, state.elements);
}
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> res;
};

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span></span></pre>
</div>
<p><span style="font-size: 14px">节点数据的增删改就完成了,只要保证在所有需要展示节点信息的地方(画布节点、弹窗表单、连线弹窗)都通过 flowData 获取,维护起来就会很轻松</span></p>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 14px">&nbsp;</span></p>
<p><span style="font-size: 16px"><strong>四、弹窗表单</strong></span></p>
<p><span style="font-size: 14px">最后再聊一聊关于弹窗表单的设计</span></p>
<p><span style="font-size: 14px">一开始设计 state 的时候就提到过,整个画布只有一个弹窗,为此还专门维护了一份 modalConfig</span></p>
<p><span style="font-size: 14px">弹窗可以只有一个,但不同类型的节点对应的表单却各有不同,这时候就需要创建不同的表单组件,通过节点类型来切换</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px"><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Modal/index.jsx</span>
<span style="color: rgba(0, 0, 0, 1)">
import React, { useContext, useRef } </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">react</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
import { Modal } </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">antd</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
import RelationNodeForm </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">./RelationNodeForm</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
import { FlowContext, Actions } </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">../../context</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 通过节点类型来切换对应的表单组件</span>
<span style="color: rgba(0, 0, 255, 1)">const</span> componentsMap =<span style="color: rgba(0, 0, 0, 1)"> {
relation: RelationNodeForm,
};

export </span><span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)"> function FlowModal() {
</span><span style="color: rgba(0, 0, 255, 1)">const</span> formRef =<span style="color: rgba(0, 0, 0, 1)"> useRef();
</span><span style="color: rgba(0, 0, 255, 1)">const</span> { state, dispatch } =<span style="color: rgba(0, 0, 0, 1)"> useContext(FlowContext);
</span><span style="color: rgba(0, 0, 255, 1)">const</span> { modalConfig } =<span style="color: rgba(0, 0, 0, 1)"> state;

</span><span style="color: rgba(0, 0, 255, 1)">const</span> handleOk = () =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 组件内部需要暴露一个 submit 方法</span>
    formRef.current.submit().then(() =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      dispatch({ type: Actions.CLOSE_MODAL });
    });
};

</span><span style="color: rgba(0, 0, 255, 1)">const</span> handleCancel = () =&gt;<span style="color: rgba(0, 0, 0, 1)"> dispatch({ type: Actions.CLOSE_MODAL });

</span><span style="color: rgba(0, 0, 255, 1)">const</span> Component =<span style="color: rgba(0, 0, 0, 1)"> componentsMap;

</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
    </span>&lt;Modal title=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">编辑节点</span><span style="color: rgba(128, 0, 0, 1)">"</span> visible={modalConfig.visible} onOk={handleOk} onCancel={handleCancel}&gt;<span style="color: rgba(0, 0, 0, 1)">
      {Component </span>&amp;&amp; &lt;Component <span style="color: rgba(0, 0, 255, 1)">ref</span>={formRef} /&gt;<span style="color: rgba(0, 0, 0, 1)">}
    </span>&lt;/Modal&gt;<span style="color: rgba(0, 0, 0, 1)">
);
}</span></span></pre>
</div>
<p><span style="font-size: 14px">但不同的表单组件,最后都是通过弹窗 footer 上的“确定”按钮来提交,而提交表单的逻辑却有可能不同</span></p>
<p><span style="font-size: 14px">我这里的做法是,在表单组件内部暴露一个 submit 方法,通过弹窗的 onOk 回调触发</span></p>
<div class="cnblogs_code">
<pre><span style="font-size: 14px"><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Modal/RelationNodeForm.jsx</span>
<span style="color: rgba(0, 0, 0, 1)">
import React, { useContext, useEffect, useImperativeHandle } </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">react</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
import { Input, Form } </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">antd</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
import { FlowContext, Actions } </span><span style="color: rgba(0, 0, 255, 1)">from</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">../../context</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;

function RelationNodeForm(props, </span><span style="color: rgba(0, 0, 255, 1)">ref</span><span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">const</span> { state, dispatch } =<span style="color: rgba(0, 0, 0, 1)"> useContext(FlowContext);
</span><span style="color: rgba(0, 0, 255, 1)">const</span> { flowData, modalConfig } =<span style="color: rgba(0, 0, 0, 1)"> state;
</span><span style="color: rgba(0, 0, 255, 1)">const</span> =<span style="color: rgba(0, 0, 0, 1)"> Form.useForm();

</span><span style="color: rgba(0, 0, 255, 1)">const</span> initialValues = flowData.<span style="color: rgba(0, 0, 255, 1)">get</span>(modalConfig.nodeId) ||<span style="color: rgba(0, 0, 0, 1)"> {};

useImperativeHandle(</span><span style="color: rgba(0, 0, 255, 1)">ref</span>, () =&gt;<span style="color: rgba(0, 0, 0, 1)"> ({
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将 submit 方法暴露给父组件</span>
    submit: () =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
      </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> form
      .validateFields()
      .then((values) </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
          dispatch({
            type: Actions.SET_FLOW_NODE,
            payload: {
            id: modalConfig.nodeId,
            ...values,
            },
          });
      })
      .</span><span style="color: rgba(0, 0, 255, 1)">catch</span>((err) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
          </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
      });
    },
}));

useEffect(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    form.resetFields();
}, );

</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> (
    </span>&lt;Form form={form} initialValues={initialValues}&gt;<span style="color: rgba(0, 0, 0, 1)">
       {</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> Form.Item </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">}
    </span>&lt;/Form&gt;<span style="color: rgba(0, 0, 0, 1)">
);
}

export </span><span style="color: rgba(0, 0, 255, 1)">default</span> React.forwardRef(RelationNodeForm);</span></pre>
</div>
<p><span style="font-size: 14px">&nbsp;</span></p>
<hr>
<p><span style="font-size: 14px">关于 React Flow 的实战就到这里了,本文介绍的是状态管理,所以很多业务代码就没有贴出来</span></p>
<p><span style="font-size: 14px">有需要的可以看下 GitHub 上的代码,仓库地址在本文的开头已经贴出来了</span></p>
<p><span style="font-size: 14px">总的来说 React Flow 用起来还是挺方便,配合良好的状态管理体系,应该能适用于大部分的流程图需求</span></p>
<p><span style="font-size: 14px">如果以后遇到了相当复杂的场景,我会再分享出来~</span></p>
<p><span style="font-size: 14px">&nbsp;</span></p><br><br>
来源:https://www.cnblogs.com/wisewrong/p/15638354.html
頁: [1]
查看完整版本: React Flow 实战(三)—— 使用 React.context 管理流程图数据