需求千千萬,流程圖常在
沒想到多年以后,我再次遇到一個關於流程圖開發的需求
以前少不更事,頭鐵用 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
注意,這里的 reat-flow 版本是 9.x,更高級的版本可能存在 API 差異,參考《Migrate to v10》
然后調用組件,傳入 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
這部分內容會在后面的文章中介紹~