高可定制的流式流程引擎 react-flow-builder

4,331 阅读5分钟

简介

审批流程编排是 CRM、协同办公等产品中不可或缺的一环,飞书审批、轻流、钉钉、纷享销客等都提供了相似的流程编排能力,以下截图分别为轻流和飞书审批的流程编排界面(但都没有开源)。

轻流.png飞书审批.png

而目前开源的流程引擎中,大多数都是提供了自由绘制的能力,如 jsplumb、GoJS、X6 等,优点是场景覆盖广、灵活性高,缺点也恰好是过高的灵活性增加了流程绘制过程中出错的可能性,若使用这三个流程引擎实现上面图片的效果,需要对连线规则限制等进行大量的二次开发。

react-flow-builder 是一个高可定制的流式流程引擎,提供了较为通用的流程编排能力,提供节点注册的机制快速实现业务诉求,下图为实际使用效果。

例子

安装

yarn add react-flow-builder
或
npm install react-flow-builder

使用

import FlowBuilder from 'react-flow-builder';

完整的 API 可通过 bytedance.github.io/flow-builde… 查看,接下来会对几个业务中高频使用的场景进行举例。

节点注册

通过 registerNode 属性注册流程引擎中需要使用的节点。从宏观上可以区分为 5 个类型的节点:

  • 开始节点 指定 isStart: true 可以将这个节点声明为开始节点,流式流程引擎的开始节点只有一个,所以不会出现在可添加节点列表中。
  • 结束节点 指定 isEnd: true 可以将这个节点声明为结束节点,流式流程引擎的结束节点也只有一个,所以同样不会出现在可添加节点列表中,且结束节点之后不会也不应该继续添加节点。
  • 分支节点、条件节点 分支节点和条件节点是一一绑定的,但没有约束只能有一个分支节点或只能有一个条件节点,若指定了 conditionNodeType 属性为某个注册的节点类型,则声明了一对分支-条件节点,但比较特殊的是,条件节点不会单独出现在可添加节点列表中,只能在分支节点内部添加。
  • 其他普通节点 其他的节点则都归纳为普通的节点,可以出现在可添加节点列表中。

自定义展示

不同的业务场景、UI 规范对节点如何展示都有不同的诉求,通过 displayComponent 可以指定该节点对应的展示组件,向自定义组件提供了 nodereadonly 等属性。

/* index.css */
.start-node, .end-node {
  height: 64px;
  width: 64px;
  border-radius: 50%;
  line-height: 64px;
  color: #fff;
  text-align: center;
}
.start-node {
  background-color: #338aff;
}
.end-node {
  background-color: #666;
}
.common-node, .condition-node {
  width: 224px;
  border-radius: 4px;
  color: #666;
  background: #fff;
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.08);
}
.common-node {
  height: 118px;
  padding: 16px;
  display: flex;
  flex-direction: column;
}
.condition-node {
  height: 44px;
  padding: 12px 16px;
}
// index.tsx
import React, { useState } from 'react';
import FlowBuilder, {
  INode,
  IRegisterNode,
  IDisplayComponent,
} from 'react-flow-builder';

import './index.css';

const StartNodeDisplay: React.FC<IDisplayComponent> = ({ node }) => {
  return <div className="start-node">{node.name}</div>;
};

const EndNodeDisplay: React.FC<IDisplayComponent> = ({ node }) => {
  return <div className="end-node">{node.name}</div>;
};

const CommonNodeDisplay: React.FC<IDisplayComponent> = ({ node }) => {
  return <div className="common-node">{node.name}</div>;
};

const ConditionNodeDisplay: React.FC<IDisplayComponent> = ({ node }) => {
  return <div className="condition-node">{node.name}</div>;
};

const registerNodes: IRegisterNode[] = [
  {
    type: 'start',
    name: '开始节点',
    displayComponent: StartNodeDisplay,
    isStart: true,
  },
  {
    type: 'end',
    name: '结束节点',
    displayComponent: EndNodeDisplay,
    isEnd: true,
  },
  {
    type: 'common',
    name: '普通节点',
    displayComponent: CommonNodeDisplay,
  },
  {
    type: 'condition',
    name: '条件节点',
    displayComponent: ConditionNodeDisplay,
  },
  {
    type: 'branch',
    name: '分支节点',
    conditionNodeType: 'condition',
  },
];

const Demo = () => {
  const [nodes, setNodes] = useState<INode[]>([]);
  const handleChange = (nodes: INode[]) => {
    setNodes(nodes);
  };
  return (
    <div>
      <FlowBuilder
        nodes={nodes}
        onChange={handleChange}
        registerNodes={registerNodes}
      />
    </div>
  );
};

export default Demo;
demo1.pngdemo2.png

自定义可添加节点列表

默认情况下,除结束节点之外的节点的下方加号中的可添加节点列表都是相同的(普通节点和分支节点),通过 addableNodeTypes 可以指定节点的下方加号中的可添加节点列表,实现可添加节点的内容差异化

// index.tsx
// 指定 条件节点 下方只能添加 普通节点
// 指定 分支节点 下方不能添加 任何节点
// 开始节点、普通节点 下方还是默认的可添加节点列表

const registerNodes: IRegisterNode[] = [
  {
    type: 'start',
    name: '开始节点',
    displayComponent: StartNodeDisplay,
    isStart: true,
  },
  {
    type: 'end',
    name: '结束节点',
    displayComponent: EndNodeDisplay,
    isEnd: true,
  },
  {
    type: 'common',
    name: '普通节点',
    displayComponent: CommonNodeDisplay,
  },
  {
    type: 'condition',
    name: '条件节点',
    displayComponent: ConditionNodeDisplay,
    addableNodeTypes: ['common'],
  },
  {
    type: 'branch',
    name: '分支节点',
    conditionNodeType: 'condition',
    addableNodeTypes: [],
  },
];

demo3.png

自定义加号之后的展示内容

点击加号之后出现的是 antd 的 Popover 组件,默认情况下,将可添加节点列表作为 Popover 的 content 进行展示,通过 addableComponent 属性可以自定义 content 的内容,实现可添加节点的内容和样式差异化,向自定义组件提供了 node 等属性 和 add 方法。

// index.tsx
import IAddableComponent from 'react-flow-builder';

const AddableComponent: React.FC<IAddableComponent> = ({ node, add }) => {
  return (
    <ul>
      <li onClick={() => add('branch')}>分支节点</li>
    </ul>
  );
};
const registerNodes: IRegisterNode[] = [
  {
    type: 'start',
    name: '开始节点',
    displayComponent: StartNodeDisplay,
    isStart: true,
  },
  {
    type: 'end',
    name: '结束节点',
    displayComponent: EndNodeDisplay,
    isEnd: true,
  },
  {
    type: 'common',
    name: '普通节点',
    displayComponent: CommonNodeDisplay,
    addableComponent: AddableComponent,
  },
  {
    type: 'condition',
    name: '条件节点',
    displayComponent: ConditionNodeDisplay,
  },
  {
    type: 'branch',
    name: '分支节点',
    conditionNodeType: 'condition',
  },
];

demo4.png

自定义节点表单

流程编排大部分情况下不仅仅只是为了节点初始状态的固定展示,通常情况下节点都会携带对应的业务数据与后端进行交互。通过 configComponent 属性可以自定义节点对应的表单组件,点击节点之后出现在 Drawer 中,向自定义组件提供了 node 等属性、cancelsave 方法。

/* index.css */
.has-error {
  box-shadow: 0 0 8px #ff4d4f;
}

.configuring {
  box-shadow: 0 0 8px #338aff;
}
// index.tsx
import IConfigComponent from 'react-flow-builder';
import { Form, Input, Button } from 'antd';

const CommonNodeDisplay: React.FC<IDisplayComponent> = ({ node }) => {
  return (
    <div className={`common-node ${node.configuring ? 'configuring' : ''} ${node.validateStatusError ? 'has-error' : ''}`}>
      {node.data?.name || node.name}
    </div>
  );
};

const ConditionNodeDisplay: React.FC<IDisplayComponent> = ({ node }) => {
  return (
    <div className={`condition-node ${node.configuring ? 'configuring' : ''} ${node.validateStatusError ? 'has-error' : ''}`}>
      {node.data?.name || node.name}
    </div>
  );
};
const ConfigForm: React.FC<IConfigComponent> = ({ node, cancel, save }) => {
  const [form] = Form.useForm();

  const handleSubmit = async () => {
    try {
      const values = await form.validateFields();
      save?.(values);
    } catch (error) {
      const values = form.getFieldsValue();
      save?.(values, !!error);
    }
  };

  return (
    <div>
      <Form form={form} initialValues={node.data || { name: node.name }}>
        <Form.Item name="name" label="Name" rules={[{ required: true }]}>
          <Input />
        </Form.Item>
      </Form>
      <div>
        <Button onClick={cancel}>取消</Button>
        <Button type="primary" onClick={handleSubmit}>
          确定
        </Button>
      </div>
    </div>
  );
};
const registerNodes: IRegisterNode[] = [
  {
    type: 'start',
    name: '开始节点',
    displayComponent: StartNodeDisplay,
    isStart: true,
  },
  {
    type: 'end',
    name: '结束节点',
    displayComponent: EndNodeDisplay,
    isEnd: true,
  },
  {
    type: 'common',
    name: '普通节点',
    displayComponent: CommonNodeDisplay,
    configComponent: ConfigForm,
  },
  {
    type: 'condition',
    name: '条件节点',
    displayComponent: ConditionNodeDisplay,
    configComponent: ConfigForm,
  },
  {
    type: 'branch',
    name: '分支节点',
    conditionNodeType: 'condition',
  },
];
demo5.pngdemo6.png

其他能力

除了个性化的节点注册功能之外,还内置了撤销重做缩放数据结构转换等功能,更多的具体例子可参见 bytedance.github.io/flow-builde…