需求千千萬,流程圖常在
沒想到多年以後,我再次遇到一個關於流程圖開發的需求
以前少不更事,頭鐵用 GG-Editor 搞了一次流程圖《在 React 項目中引入 GG-Editor 編輯可視化流程》,差點把自己給埋了
這次再遇到類似的需求,在各路大神的指點下,我選擇了 React Flow 來進行開發,原因如下:
1. 相比於 jsPlumb、Antv/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
這部分內容會在後面的文章中介紹~