Talk is cheap. Show me your code

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

 

posted @ 2021-11-16 19:22  Wise.Wrong  阅读(5880)  评论(3编辑  收藏  举报