zoukankan      html  css  js  c++  java
  • React Flow 实战(二)—— 拖拽添加节点

    上一篇 《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 的流程图数据管理方案

    // 文章还在施工中,有兴趣可以先看下项目 flow-demo-app

  • 相关阅读:
    python开发第二十六天CMDB
    python模块(requests,logging)
    python自动开发之第二十五天
    python自动开发之第二十四天(Django)
    python自动开发之第二十三天(Django)
    python自动开发之第二十二天
    python自动开发之第二十一天
    python自动开发之(ajax)第二十天
    python自动开发之(django)第十九天
    python自动开发之第十八天
  • 原文地址:https://www.cnblogs.com/wisewrong/p/15433647.html
Copyright © 2011-2022 走看看