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 的流程圖數據管理方案

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章