react-flow 轻量便捷的前端流程图工具

14,667 阅读7分钟

前言

相信各位前端同学在工作中都有遇到过"流程图"的需求 , 不知道大家用到了什么工具呢?

antv/v6 , jsPlumb , gojs ?

今天再为大家介绍一款 , react-flow

话不多说 , 开始整活。

依赖包版本

...
"react": "^16.10.2",
"react-flow-renderer": "^9.7.3",

demo代码结构

这里只放组件文件目录

--| ReactFlow 
--|--| Edge  自定义边
--|--| Node  自定义节点
--|--| index.js 组件入口
--|--| mock.js  测试数据

概览

image.png 如上 , 图中展示了一个相对完整的流程图 , 可由外部控制流程图的缩放,复原等 , 有缩略图展示流程图的概略 , 不难看出 , 流程图中的节点,边是存在一些定制化的 , 下面是概览的代码 , 至于定制部分后面会有详细介绍

// index.js
import React, { useState } from 'react';

import ReactFlow, {
  MiniMap,    // 缩略图
  Controls,   // 画布缩放大小控制
  Background, // 背景组件
} from 'react-flow-renderer';

import initialElements from './mock';

const onLoad = (reactFlowInstance) => {
  console.log('flow loaded:', reactFlowInstance);
  reactFlowInstance.fitView();
};

const OverviewFlow = () => {
  const [elements, setElements] = useState(initialElements);

  return (
    <ReactFlow
      elements={elements}
      onLoad={onLoad}
    >
      <MiniMap
        nodeStrokeColor={(n) => {
          if (n.type === 'input') return '#0041d0';
          if (n.type === 'output') return '#ff0072';
          if (n.type === 'default') return '#1a192b';

          return '#eee';
        }}
        nodeColor={(n) => {
          return '#fff';
        }}
        nodeBorderRadius={2}
      />
      <Controls />
      <Background color="#aaa" gap={16} />
    </ReactFlow>
  );
};

export default OverviewFlow;
// mock.js

import React from 'react';

// 测试数组中的数据整体由两部分组成(node,edge)即线和边.

// node 包含 id(节点id,唯一) , type(节点类型 , input , output , customnode...) , position(节点位置) , data(放一些节点自定义属性)

// edge 包含 id(边id,唯一) , source(当前边以哪一个节点作为起点, 这里一定是节点id) , target(当前边以哪一个节点作为终点) 

// node和edge的部分后面会做详细介绍 , 这里先做了解
export default [
  {
    id: '1',
    type: 'input',
    data: {
      label: (
        <>
          Welcome to <strong>React Flow!</strong>
        </>
      ),
    },
    position: { x: 250, y: 0 },
  },
  {
    id: '2',
    data: {
      label: (
        <>
          This is a <strong>default node</strong>
        </>
      ),
    },
    position: { x: 100, y: 100 },
  },
  {
    id: '3',
    data: {
      label: (
        <>
          This one has a <strong>custom style</strong>
        </>
      ),
    },
    position: { x: 400, y: 100 },
    style: {
      background: '#D6D5E6',
      color: '#333',
      border: '1px solid #222138',
      width: 180,
    },
  },
  {
    id: '4',
    position: { x: 250, y: 200 },
    data: {
      label: 'Another default node',
    },
  },
  {
    id: '5',
    data: {
      label: 'Node id: 5',
    },
    position: { x: 250, y: 325 },
  },
  {
    id: '6',
    type: 'output',
    data: {
      label: (
        <>
          An <strong>output node</strong>
        </>
      ),
    },
    position: { x: 100, y: 480 },
  },
  {
    id: '7',
    type: 'output',
    data: { label: 'Another output node' },
    position: { x: 400, y: 450 },
  },
  { id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' },
  { id: 'e1-3', source: '1', target: '3' },
  {
    id: 'e3-4',
    source: '3',
    target: '4',
    animated: true,
    label: 'animated edge',
  },
  {
    id: 'e4-5',
    source: '4',
    target: '5',
    arrowHeadType: 'arrowclosed',
    label: 'edge with arrow head',
  },
  {
    id: 'e5-6',
    source: '5',
    target: '6',
    type: 'smoothstep',
    label: 'smooth step edge',
  },
  {
    id: 'e5-7',
    source: '5',
    target: '7',
    type: 'step',
    style: { stroke: '#f6ab6c' },
    label: 'a step edge',
    animated: true,
    labelStyle: { fill: '#f6ab6c', fontWeight: 700 },
  },
];

节点

image.png

source code

本节是主要介绍节点 , 去掉一些与无关的代码

//index.js
import React, { useState } from 'react';

import ReactFlow from 'react-flow-renderer';

import CustomNode from "./Node";

import initialElements from './mock';

// 当我们用到自定义node的时候需要传一份nodeTypes给react-flow , 注意的是对象的key要和elements中的
// type对应
const nodeTypes = {
  customNode: CustomNode,
};

const Flow = () => {
  const [elements] = useState(initialElements);
  return (
    <ReactFlow
      elements={elements}
      nodeTypes={nodeTypes}
    />
  );
};

export default Flow;
// mock.js

export default [
  {
    id: '1',
    type: 'input',
    data: { label: 'An input node' },
    position: { x: 0, y: 50 },
    sourcePosition: 'right',
  },
  {
    id: '2',
    type: 'customNode',
    data: { color: "red" },
    dragHandle: '.custom-drag-handle',
    style: { border: '1px solid #777', padding: 10 },
    position: { x: 300, y: 50 },
  },
  {
    id: '3',
    type: 'output',
    data: { label: 'Output A' },
    position: { x: 650, y: 25 },
    targetPosition: 'left',
  },
  {
    id: '4',
    type: 'output',
    data: { label: 'Output B' },
    position: { x: 650, y: 100 },
    targetPosition: 'left',
  },

  {
    id: 'e1-2',
    source: '1',
    target: '2',
    sourceHandle: 'd',
    style: { stroke: '#000' },
  },
  {
    id: 'e2a-3',
    source: '2',
    target: '3',
    sourceHandle: 'a',
    style: { stroke: '#000' },
  },
  {
    id: 'e2b-4',
    source: '2',
    target: '4',
    sourceHandle: 'b',
    style: { stroke: '#000' },
  },
];
./node/index.js
import React, { memo } from 'react';

import { Handle } from 'react-flow-renderer';

/**
 * Handle 句柄
 * type:句柄类型 'source' | 'target'
 * id:句柄id
 * position: 句柄位置 'left', 'right', 'top' or 'bottom' handle position - default: 'top' for type target, 'bottom' for type source
 * onConnect: 连接时触发的回调函数 , 此回调仅在'source'句柄上执行 
 * style: css
 * className: 句柄类名
 * 
 * */
export default memo(({ data, isConnectable }) => {
  return (
    <>
      <div>
        自定义节点 ,{data.color}
        <span className="custom-drag-handle" style={{ fontWeight: "bold" }}>点我拖拽哦</span>
      </div>
      <Handle
        type="source"
        position="right"
        id="a"
        style={{ top: 10, background: '#555' }}
        isConnectable={isConnectable}
      />
      <Handle
        type="source"
        position="right"
        id="b"
        style={{ bottom: 10, top: 'auto', background: '#555' }}
        isConnectable={isConnectable}
      />
      <Handle
        type="target"
        position="left"
        id="d"
        style={{ bottom: 10, top: 'auto', background: '#555' }}
        isConnectable={isConnectable}
      />
    </>
  );
});

Options

参数说明图示类型默认值是否必传
id节点唯一标识--string--
position节点的位置属性--{ x: number, y: number }--
data如果使用的标准类型,则为{label:""}的形式 , 如果使用的自定义节点,则取决于你的数据形式,可以放一些数据供组件消费--object--
type节点的类型 input, pitput , default , 处上述三种标准类型外 , 也可自定义类型 , 这块即自定义了stringdefault
style节点样式--string--
className节点类名--string--
targetPosition标准模式输入句柄在节点中的位置 top , bottom , left , right--stringtop
sourcePosition标准模式输出句柄在节点中的位置 top , bottom , left , right--stringtop
isHidden当前节点是否展示--booleantrue
draggable当前节点是否可被拖动--booleantrue
connectable当前节点是否可连线--booleantrue
selectable当前节点是否可选中--booleantrue
dragHandle点击到节点内指定元素可拖动 , 注意这里是对应元素的类名--string--

image.png

线

image.png

source code

没有修改index.js 和 Node/index.js的代码 , 就不贴了

// mock.js

export default [
  {
    id: '1',
    type: 'input',
    data: { label: 'An input node' },
    position: { x: 0, y: 50 },
    sourcePosition: 'right',
  },
  {
    id: '2',
    type: 'customNode',
    data: { color: "red" },
    dragHandle: '.custom-drag-handle',
    style: { border: '1px solid #777', padding: 10 },
    position: { x: 300, y: 50 },
  },
  {
    id: '3',
    type: 'output',
    data: { label: 'Output A' },
    position: { x: 650, y: 25 },
    targetPosition: 'left',
  },
  {
    id: '4',
    type: 'default',
    data: { label: 'default A' },
    position: { x: 650, y: 100 },
  },
  {
    id: '5',
    type: 'default',
    data: { label: 'default E' },
    position: { x: 850, y: 100 },
  },
  {
    id: 'e1-2',
    source: '1',
    target: '2',
    sourceHandle: 'd',
    animated: true,
    type: 'smoothstep',
    style: { stroke: '#000' },
  },
  {
    id: 'e2a-3',
    source: '2',
    target: '3',
    sourceHandle: 'a',
    type: 'step',
    style: { stroke: '#000' },
  },
  {
    id: 'e2a-5',
    source: '2',
    target: '5',
    type: 'straight',
    label: 'label',
    labelShowBg: true,
    labelBgPadding: [20, 10]
  },
  {
    id: 'e2b-4',
    source: '2',
    target: '4',
    sourceHandle: 'b',
    type: 'customEdge',
    style: { stroke: '#000' },
    data: { text: 'custom edge' },
    arrowHeadType: 'arrow',
  },
];
// ./Edge/index.js
import React from 'react';
import { getBezierPath, getEdgeCenter } from 'react-flow-renderer';

export default function CustomEdge({
  id,
  sourceX,
  sourceY,
  targetX,
  targetY,
  sourcePosition,
  targetPosition,
}) {
  const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });
  const [edgeCenterX, edgeCenterY] = getEdgeCenter({
    sourceX,
    sourceY,
    targetX,
    targetY,
  });

  return (
    <>
      <path id={id} className="react-flow__edge-path" d={edgePath} />

      {/* 
        这里是在线上添加一些其他的元素 , 因为react-flow 线使用svg画的 , 
        所以我们需要在线中添加一些其他元素的话按照svg的语法格式来即可

        至于一些关于位置的信息 , react-flow提供了如下信息:
        sourceX:来源节点x坐标
        sourceY:来源节点y坐标
        targetX:目标节点x坐标
        targetY:目标节点y坐标
        edgeCenterX:线中心x坐标
        edgeCenterY:线中心y坐标
       */}
      <foreignObject
        width={40}
        height={40}
        x={edgeCenterX - 40 / 2}
        y={edgeCenterY - 40 / 2}
      >
        <button>按钮</button>
      </foreignObject>

    </>
  );
}

Options

参数说明类型默认值是否必传
id线唯一标识string--
source线起点(即来源节点id)string--
target线终点(即目标节点id)string--
sourceHandle线起点句柄(即来源节点句柄id) , 在自定义节点的情况下string--
targetHandle线终点句柄(即目标节点句柄id) , 在自定义节点的情况下string--
className线类名string--
type线的类型 , 图中有标, straight, step , smoothstepstringsmoothstep
animated线是否有虚线动画效果booleanfalse
label线中的文字string--
labelStyle线中的文字样式string--
labelShowBg是否显示label的背景 , 默认有白色的背景booleantrue
labelBgStylelabel的样式string--
arrowHeadType线末端是箭头的样式 , arrow or arrowclosedstring--
data自定义线,放一些数据供组件消费object--

ReactFlowProvider

通常我们在开发中,是需要一些流程图的其他信息的,react-flow帮我们实现了数据上的管理 , 实际上不仅整合了数据 , 也有一些actions供开发者使用 , 前提是在使用这些数据或者执行某一些actions的时候需要使用ReactFlowProvider对我们react-flow进行包裹

source code

如下,我们分别获取了点和线的数据 , 可以在需要的时候使用

//index.js

import React, { useState } from 'react';

import ReactFlow, { ReactFlowProvider, useStoreState } from 'react-flow-renderer';

import CustomNode from "./Node";

import CustomEdge from "./Edge";

import initialElements from './mock';

const nodeTypes = {
  customNode: CustomNode,
};

const edgeTypes = {
  customEdge: CustomEdge,
};

const Flow = () => {
  // 所有边
  const edges = useStoreState(state => state.edges);

  // 所有点
  const node = useStoreState(state => state.nodes);

  const [elements, setElements] = useState(initialElements);


  return (
    <>
      <button onClick={() => setElements((els) => els.filter(el => el.id !== "1"))}>remove one node</button>
      <ReactFlow
        elements={elements}
        nodeTypes={nodeTypes}
        edgeTypes={edgeTypes}
      />
    </>
  );
};

const FlowWithProvider = () => {
  return (
    <ReactFlowProvider>
      <Flow />
    </ReactFlowProvider>
  )
}
export default FlowWithProvider;

小结

以上,介绍了react-flow的基础用法。通过自定义节点自定义线的一部分来看在定制化方面还是非常灵活的。 如果大家有在项目中遇到流程图相关的需求的时候可以用上react-flow

再贴下官方链接

demo

docs