輕量化流程圖開發,比 X6 清爽太多 —— React Flow 實戰(一)

需求千千萬,流程圖常在

沒想到多年以後,我再次遇到一個關於流程圖開發的需求

以前少不更事,頭鐵用 GG-Editor 搞了一次流程圖《在 React 項目中引入 GG-Editor 編輯可視化流程》,差點把自己給埋了

這次再遇到類似的需求,在各路大神的指點下,我選擇了 React Flow 來進行開發,原因如下:

1. 相比於 jsPlumbAntv/X6 而言,React Flow 的技術相對先進

  // 小聲BB,X6 居然用到了 jquery: https://github.com/antvis/X6/blob/master/packages/x6/package.json#L70

2. 高度自定義,任何 ReactElement 都可以作爲節點

3. API 真的超級簡單,而且體積不大,npm 3.9 MB

 

 

一、快速上手

首先在項目中安裝依賴

yarn add react-flow-renderer

然後調用組件,傳入 elements 就能渲染出一個流程圖

import React from "react";
import ReactFlow from "react-flow-renderer";

const elements = [
  // node
  {
    id: "1",
    data: {
      label: 'Node 1',
    },
    position: { x: 250, y: 25 },
  },
  {
    id: "2",
    data: {
      label: 'Node 2',
    },
    position: { x: 100, y: 125 },
  },
  {
    id: "3",
    data: {
      label: 'Node 3',
    },
    position: { x: 250, y: 250 },
  },
  // edge
  { id: "e1-2", source: "1", target: "2" },
  { id: "e2-3", source: "2", target: "3" },
];

export default function Demo() {
  return (
    <div style={{ height: 300 }}>
      <ReactFlow elements={elements} />
    </div>
  );
}

這裏的 elements 是一個包含節點 node 連線 edge 的對象數組,他們在數據結構上有以下特點:

 

node

- id: string  唯一標識,用於連線,必填

position: { x: number, y: number }  定位信息,必填

- type: string  定義節點的類型,可以是 React Flow 提供的 'default' | 'input' | 'output',也可以是自定義類型

- data: {} 傳入節點內的數據,根據實際的節點類型 type 傳入

// 完整的配置項可以查看官網 Node Options

每一個節點都必須含有一個 id 和 postion,自定義節點必須傳入 type

 

edge:

- id: string  唯一標識,必填

source: string  連線的起始節點的 id,必填

- target: string  連線的結束節點的 id,必填

- type: string  線的類型,React Flow 提供了貝塞爾曲線 bezier直線 straight折線 step帶圓角的折線 smoothstep,也支持自定義連線 

// 完整的配置項可以查看官網 Edge Options

線就很好理解,只需要起點 source 和終點 target 就能完成連線

 

React Flow 還提供了兩個工具方法來判斷 elements 中的元素是節點還是連線

import { isNode, isEdge } from 'react-flow-renderer';

掌握了“點”與“線”的基本概念,流程圖就能信手拈來

但產品經理可不會認同 React Flow 提供的默認節點類型,所以自定義節點就成了必修課

 

 

二、自定義節點

在上面介紹的 node 數據中,可以傳入一個 data,這個 data 會傳入節點組件中的 props

假如我們需要做一個這樣的節點

可以先寫按圖寫一個這樣的 ReactNode

import React from "react";
import { isArray } from "lodash";

// data 會從 elements 數據源傳入
const ListNode = ({ data }) => {
  const { title, list } = data || {};
  return (
    <div className="flow-node list-node">
      <div className="list-node_title">
        <span className="list-node_title__inner">{title}</span>
      </div>
      <ul className="list-node_content">
        {
          isArray(list) && list.map((x, i) => (
            <li className="list-node__item" key={i}>
              <span className="list-node__item_label">{x.name}</span>
              <span className="list-node__item_type">{x.type}</span>
            </li>
          ))
        }
      </ul>
    </div>
  );
};

export default React.memo(ListNode);

通過傳入的 data 就能渲染出這個節點的樣式,接下來解決連線的問題

 

ReactFlow 節點的連線是通過 Handle 組件實現的

Handle 其實就是節點上的“連接點”,需要多少個連接點,就可以在組件裏寫多少個 <Handle />

它可以接收的 props 參數有:

type:  string 連接點類型,可選值爲 出口 'source' | 入口 'target'必填

position:  string 連接點的位置,有四個可選值 'left' | 'right' | 'top' | 'bottom'

style: object 連接點的樣式,除了常見的寬高、顏色之外,還可以通過定位對 position 進行微調

id: string 如果節點中存在存在多個 source Handle 或者多個 target Handle, 可以通過 id 來精準控制連線的起點和終點

isConnectable: boolean 是否允許連線,可以從節點的 props 中獲取

 

比如節點左上角的連接點,就可以這麼寫:

import React from "react";
import { Handle } from "react-flow-renderer";

const nodeBaseStyle = {
  background: "#0FA9CC",
  width: '8px',
  height: '8px',
};

const nodeLeftTopStyle = {
  ...nodeBaseStyle,
  top: 60,
};

const ListNode = ({ data, isConnectable = true }) => {
  return (
    <div className="flow-node list-node">
      <div className="list-node_title">
        {/* ... */}
      </div>
      <ul className="list-node_content">
        {/* ... */}
      </ul>
      <Handle
        type="target"
        position="left"
        id="lt"
        style={nodeLeftTopStyle}
        isConnectable={isConnectable}
      />
    </div>
  );
};

export default React.memo(ListNode);

其他的節點也是以同樣的方式添加,注意定義好 type,因爲連線只能從 source 連接到 target


 

現在自定義節點已經開發好了,在使用的時候需要先註冊,也就是向 <ReactFlow /> 傳入一個 nodeTypes

然後在使用的時候,需要在 elements 中聲明節點 node 的類型,以及連線 edge 的起點和終點 

const elements = [
  // nodes
  {
    id: '1',
    // 聲明節點類型
    type: "list",
// data 會作爲 props 傳給節點 data: { title:
'節點-1', list: [], }, isConnectable: true, position: { x: 220, y: 65 }, }, { id: '2', // 聲明節點類型 type: "list",
// data 會作爲 props 傳給節點 data: { title:
'節點-2', list: [], }, isConnectable: true, position: { x: 395, y: 260 }, }, // edges { id: "egde1-2", type: "step", // 起始節點 id source: "1", // 起點 Handle id sourceHandle: "b", // 結束節點 id target: "2", // 終點 Handle id targetHandle: "lt", }, ];

 

 

三、自定義連線

ReactFlow 提供的默認連線可以設置 label

// 圖示流程圖的完整示例可以參考這裏

 

但 label 只能設置文本,如果要在連線中間加一個按鈕,就需要自定義連線

ReactFlow edge 是通過 svg 繪製的,所以自定義連線本身也是一個 <path />

爲了更方便的繪製 path,ReactFlow 提供了一些工具方法

import {
  // 繪製貝塞爾曲線
  getBezierPath,
  // 繪製帶圓角的折線
  getSmoothStepPath,
  // 計算出連線的中點
  getEdgeCenter,
  // 繪製連線末端的箭頭
  getMarkerEnd,
} from "react-flow-renderer";

通過這些方法,就能很方便的繪製出自定義連線

const CustomEdge = ({
  id,
  sourceX,
  sourceY,
  targetX,
  targetY,
  sourcePosition,
  targetPosition,
  borderRadius = 0,
  style = {},
  data,
  arrowHeadType,
  markerEndId,
}) => {
  const edgePath = getSmoothStepPath({
    sourceX,
    sourceY,
    sourcePosition,
    targetX,
    targetY,
    targetPosition,
    borderRadius,
  });

  const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);

  return (
    <>
      <path
        id={id}
        style={style}
        className="custom-edge"
        d={edgePath}
        markerEnd={markerEnd}
      />
    </>
  );
}

接下來是連線中點的按鈕,爲了在 svg 裏添加 button,就需要使用 foreignObject

再通過 getEdgeCenter 獲取到連線的中點,就可以繪製按鈕了

const foreignObjectSize = 24;

const CustomEdge = ({
  id,
  sourceX,
  sourceY,
  targetX,
  targetY,
}) => {
  const [edgeCenterX, edgeCenterY] = getEdgeCenter({
    sourceX,
    sourceY,
    targetX,
    targetY,
  });

  const onEdgeClick = (evt, id) => {
    evt.stopPropagation();
    console.log(`click ${id}`);
  };

  return (
    <>
      <path />
      <foreignObject
        width={foreignObjectSize}
        height={foreignObjectSize}
        x={edgeCenterX - foreignObjectSize / 2}
        y={edgeCenterY - foreignObjectSize / 2}
        className="custom-edge-foreignobject"
      >
        <button onClick={(event) => onEdgeClick(event, id)} />
      </foreignObject>
    </>
  );
}

// 完整代碼可以查看官方的 Edge with Button 示例

 

和節點的 nodeTypes 一樣,自定義的連線也需要通過 edgeTypes 來註冊

並且在 elements 中需要設置對應的連線類型

const elements = [
  // node
  // ...
  // edges
  {
    id: "egde1-2",
    type: "link", // 使用自定義連線
    source: "1",
    target: "2",
  },
]

 

 

掌握了自定義節點和自定義連線之後,就能隨意的繪製流程圖了

但上面傳給 <ReactFlow /> 的 elements 都是一開始寫好的假數據

如果要開發一個真實的流程圖,肯定需要數據交互,這就需要用到 ReactFlowProvider

這部分內容會在後面的文章中介紹~

 

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