介绍
Rete.js(读作 /ˈriː.ti/,在意大利语中意为“网络”)是一个用于创建可视化界面和工作流的框架。
它主要提供两方面能力:
可视化
Rete.js 支持使用多种前端框架来渲染编辑器,包括 React、Vue、Angular、Svelte 和 Lit。
开发者可以为节点、端口、控件和连线创建自定义组件,不同框架的组件甚至可以在同一个编辑器中同时使用。
图处理(执行)
Rete.js 提供多种用于处理图结构的引擎,支持基于 数据流(Dataflow) 和 控制流(Control Flow) 的执行方式。
不同类型的处理引擎可以在同一个图中组合使用。
在此之前我对比了其他相关框架,例如 React Flow、AntV G6 等,但是并不适合我项目需要的场景。
快速开始
初始化项目
首先用 vite 或者其他工具初始化一个 React 项目,如果你不想用 React,可以参照官方文档 https://retejs.org/docs/getting-startedhttps://retejs.org/docs/getting-started 创建其他框架的 Rete.js 项目。
然后添加相关依赖:
npm i rete rete-area-plugin rete-connection-plugin rete-render-utils rete-react-plugin
可选配置(Vite)
如果你用的是 Vite 可以尝试下面的配置,引入 Tailwind CSS 并配置 Ailas
引入 Tailwind CSS Vite 依赖:
npm install tailwindcss @tailwindcss/vite
然后配置 vite.config.ts:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite';
import path from "path";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})
然后在 index.css 或其他顶级 css 文件中引入 @import "tailwindcss";
最后再修改一下 tsconfig.ts:
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
这样就可以在项目中使用 @/ 作为 src 路径了。
基本概念
在开始之前,需要先介绍一下 Rete 框架绘图的几个基本概念:
- 节点(Node):图中的不同节点
- 插槽(Socket):节点上输入/输出的端点
- 连接(Connection):连接节点之间插槽的连线
而 Rete 框架支持自定义上述类型,因此提供了一个名为 GetSchemes 的类型来表示整个绘图系统的样式。
export type GetSchemes<NodeData extends NodeBase, ConnectionData extends ConnectionBase> = {
Node: NodeData;
Connection: ConnectionData;
};
从这个类型可以看出,框架允许我们自己拓展节点、连接的子类,这样就能保证框架中绘制出的元素的数据类型(Node、Socket、Connection)也能同时满足我们业务的需要。
简单的例子
导入模块
import { createRoot } from "react-dom/client";
import { NodeEditor, type GetSchemes, ClassicPreset } from "rete";
import { AreaPlugin, AreaExtensions } from "rete-area-plugin";
import {
ConnectionPlugin,
Presets as ConnectionPresets,
} from "rete-connection-plugin";
import {ReactPlugin, Presets, type ReactArea2D, useRete} from "rete-react-plugin";
定义基本 Scheme
type Schemes = GetSchemes<
ClassicPreset.Node,
ClassicPreset.Connection<ClassicPreset.Node, ClassicPreset.Node>
>;
type AreaExtra = ReactArea2D<Schemes>;
创建编辑器
async function createEditor(container: HTMLElement) {
// 定义插槽类型
const socket = new ClassicPreset.Socket("socket");
const editor = new NodeEditor<Schemes>();
const area = new AreaPlugin<Schemes, AreaExtra>(container);
// 节点连接器
const connection = new ConnectionPlugin<Schemes, AreaExtra>();
// 节点渲染器
const render = new ReactPlugin<Schemes, AreaExtra>({ createRoot });
// 节点选择
AreaExtensions.selectableNodes(area, AreaExtensions.selector(), {
accumulating: AreaExtensions.accumulateOnCtrl(),
});
// 配置节点渲染器
render.addPreset(Presets.classic.setup());
// 配置节点连接器
connection.addPreset(ConnectionPresets.classic.setup());
editor.use(area);
area.use(connection);
area.use(render);
AreaExtensions.simpleNodesOrder(area);
// 创建节点 A
const a = new ClassicPreset.Node("A");
a.addControl("a", new ClassicPreset.InputControl("text", { initial: "a" }));
a.addOutput("a", new ClassicPreset.Output(socket));
await editor.addNode(a);
// 创建节点 B
const b = new ClassicPreset.Node("B");
b.addControl("b", new ClassicPreset.InputControl("text", { initial: "b" }));
b.addInput("b", new ClassicPreset.Input(socket));
await editor.addNode(b);
// 将 A 节点的插槽 a 连接到 B 节点的插槽 b
await editor.addConnection(new ClassicPreset.Connection(a, "a", b, "b"));
// 移动节点的位置
await area.translate(a.id, { x: 0, y: 0 });
await area.translate(b.id, { x: 270, y: 0 });
setTimeout(() => {
// wait until nodes rendered because they dont have predefined width and height
AreaExtensions.zoomAt(area, editor.getNodes());
}, 10);
return {
destroy: () => area.destroy(),
};
}
App 组件
function App() {
const [ref] = useRete(createEditor);
return (
<>
<div>
<div ref={ref} style={{ height: "100vh", width: "100vw" }}></div>
</div>
</>
)
}
运行测试
现在可以 npm run dev 把项目运行起来看一下效果:

封装 Rete.js
通过上面简单的例子发现,要绘制出节点与连线并不难,而是如何在此基础上进行拓展。
定义插槽
IGraphSocket.ts
import {ClassicPreset} from "rete";
export interface IGraphSocket {
isCompatibleWith(socket: ClassicPreset.Socket): boolean;
}
BaseGraphSocket.ts
import {ClassicPreset} from "rete";
import type {IGraphSocket} from "./IGraphSocket.ts";
export abstract class BaseGraphSocket extends ClassicPreset.Socket implements IGraphSocket {
protected constructor(socketId: string) {
super(socketId);
}
abstract isCompatibleWith(socket: ClassicPreset.Socket): boolean
}
这里的 BaseGraphSocket 将作为后续开发自定义插槽的基类,不再使用 ClassicPreset.Socket 类。
socket-utils.ts
import {ClassicPreset, NodeEditor} from "rete";
import type {BaseGraphSchemes} from "../types/schemes.ts";
import type {BaseGraphSocket} from "../socket/BaseGraphSocket.ts";
import {BaseGraphNode} from "../node/BaseGraphNode.ts";
import {BaseGraphNodeConnection} from "../types/connection.ts";
type Input<S extends BaseGraphSocket> = ClassicPreset.Input<S>;
type Output<S extends BaseGraphSocket> = ClassicPreset.Output<S>;
/**
* 从某个连接获取相关源节点与目标节点上的插槽类型
*
* @param editor NodeEditor
* @param connection
*/
export function getConnectionSockets<
S extends BaseGraphSocket,
N extends BaseGraphNode<S>,
C extends BaseGraphNodeConnection<S, N, N>,
SCHEMES extends BaseGraphSchemes<S, N, C>
>(
editor: NodeEditor<SCHEMES>,
connection: SCHEMES["Connection"]
): { source?: S, target?: S } {
const source = editor.getNode(connection.source);
const target = editor.getNode(connection.target);
const output =
source &&
(source.outputs as Record<string, Output<S>>)[connection.sourceOutput];
const input =
target && (target.inputs as Record<string, Input<S>>)[connection.targetInput];
return {
source: output?.socket,
target: input?.socket
};
}
定义节点
BaseGraphNode.ts
import {ClassicPreset} from "rete";
import {BaseGraphSocket} from "../socket/BaseGraphSocket.ts";
export abstract class BaseGraphNode<S extends BaseGraphSocket> extends ClassicPreset.Node<
Record<string, NonNullable<S>>,
Record<string, NonNullable<S>>,
object
> {
protected constructor(nodeType: string) {
super(nodeType);
}
public addInputSocket(label: string, socket: S, displayName?: string) {
this.addInput(label, new ClassicPreset.Input<S>(socket, displayName))
}
public removeInputSocket(label: string) {
for (const input in this.inputs) {
if (input == label) {
this.removeInput(input);
}
}
}
public clearInputs() {
for (const input in this.inputs) {
this.removeInput(input);
}
}
public addOutputSocket(label: string, socket: S, displayName?: string) {
this.addOutput(label, new ClassicPreset.Output<S>(socket, displayName))
}
public removeOutputSocket(label: string) {
for (const output in this.outputs) {
if (output == label) {
this.removeOutput(output);
}
}
}
public clearOutputs() {
for (const output in this.outputs) {
this.removeOutput(output);
}
}
}
这里的 BaseGraphNode 同上,替换原来的 ClassicPreset.Node 类。
定义 Schemes
connections.ts
import {ClassicPreset} from "rete";
import type {BaseGraphNode} from "../node/BaseGraphNode.ts";
import type {BaseGraphSocket} from "../socket/BaseGraphSocket.ts";
export class BaseGraphNodeConnection<
S extends BaseGraphSocket,
A extends BaseGraphNode<S>,
B extends BaseGraphNode<S>
> extends ClassicPreset.Connection<A, B> {}
schemes.ts
import type {BaseGraphNode} from "../node/BaseGraphNode.ts";
import {type GetSchemes} from "rete";
import {BaseGraphNodeConnection} from "./connection.ts";
import type {BaseGraphSocket} from "../socket/BaseGraphSocket.ts";
export type BaseGraphSchemes<
S extends BaseGraphSocket,
N extends BaseGraphNode<S>,
C extends BaseGraphNodeConnection<S, N, N>
> = GetSchemes<N, C>;
配置连接器
连接器顾名思义就是用来连接节点之间插槽的组件,在 Rete 里作为一个插件存在,名为:ConnectionPlugin
如果要连接插槽或阻止非法连接,就需要通过配置连接器来实现。
下面是具体的实现方法:
connection.addPreset(() => {
return new ClassicFlow({
// 判断两个插槽是否可以连接
canMakeConnection(from: SocketData, to: SocketData) {
const [source, target] = getSourceTarget(from, to) || [null, null];
if (!source || !target || from === to) return false;
const sourceNode = editor.getNode(source.nodeId)!;
const targetNode = editor.getNode(target.nodeId)!;
const sockets = getConnectionSockets(
editor,
props.connectionFactory(
sourceNode,
source.key,
targetNode,
target.key
)
);
if (!sockets.source!.isCompatibleWith(sockets.target!)) {
props.events?.onInvalidConnection?.();
connection.drop();
return false;
}
const connected = editor
.getConnections()
.find((conn) => conn.source == sourceNode.id &&
conn.target == targetNode.id &&
conn.sourceOutput == source.key &&
conn.targetInput == target.key
) != null;
// Already connected before
if (connected) {
connection.drop();
return false;
}
return Boolean(source && target);
},
// 在 canMakeConnection 校验通过后调用此方法建立连接
makeConnection(from: SocketData, to: SocketData, context) {
const [source, target] = getSourceTarget(from, to) || [null, null];
const { editor } = context;
if (source && target) {
void editor.addConnection(
props.connectionFactory(
editor.getNode(source.nodeId)!,
source.key,
editor.getNode(target.nodeId)!,
target.key
)
);
return true;
}
}
});
});
配置渲染器
如果想要自定义绘制元素的外观,就不能再使用 render.addPreset(Presets.classic.setup()); 提供的默认配置,和上面的连接器一样,需要自定义配置:
// 配置渲染器
render.addPreset(
Presets.classic.setup({
customize: {
node(data) {
return ({ emit }) => {
return props.render?.node(data.payload, emit) ?? <Presets.classic.Node data={data.payload} emit={emit} />
}
},
connection(data) {
// const { source, target } = getConnectionSockets(editor, data.payload);
return () => {
return props.render?.connection(data.payload) ?? <Presets.classic.Connection data={data.payload} />
};
},
socket(data) {
return () => {
return props.render?.socket(data.payload as S) ?? <Presets.classic.Socket data={data.payload} />
}
}
},
})
);
这里只是给出一个示例,实际上返回的是一个 JSX.Element,你可以改成自己想要的样式,而 data.payload 对应的是 Schemes 中的类型,例如在 node 中的 data.payload 的类型就是 N extends BaseGraphNode,以此类推。
封装编辑器
接下来就是最重要的部分了,把原来的 createEditor 封装一下,对外暴露编辑器内的插件,同时支持从参数配置内部插件。
CreateGraphEditorProps 作为 createEditor 的参数,对外提供自定义渲染、事件监听的能力:
export interface CreateGraphEditorProps<
S extends BaseGraphSocket,
N extends BaseGraphNode<S>,
C extends BaseGraphNodeConnection<S, N, N>,
SCHEMES extends BaseGraphSchemes<S, N, C>
> {
connectionFactory: (fromNode: N, fromSocket: string, toNode: N, toSocket: string) => SCHEMES["Connection"],
render?: {
node?: (node: SCHEMES["Node"], emit: RenderEmit<SCHEMES>) => React.JSX.Element;
connection?: (connection: SCHEMES["Connection"]) => React.JSX.Element;
socket?: (socket: S) => React.JSX.Element;
},
events?: {
onInvalidConnection?: () => void
}
}
GraphEditorContext 将作为 createEditor 的返回参数,对外提供 Rete 的组件以及销毁方法:
export interface GraphEditorContext<
S extends BaseGraphSocket,
N extends BaseGraphNode<S>,
C extends BaseGraphNodeConnection<S, N, N>,
SCHEMES extends BaseGraphSchemes<S, N, C>
> {
// Rete 组件
rete: {
editor: NodeEditor<SCHEMES>;
area: AreaPlugin<SCHEMES, ReactArea2D<SCHEMES>>;
connection: ConnectionPlugin<SCHEMES, ReactArea2D<SCHEMES>>;
};
// 自适应缩放
autoFitViewport(): void;
// 销毁方法
destroy(): void;
}
接下来就是 createBaseGraphEditor 方法:
async function createBaseGraphEditor<
S extends BaseGraphSocket,
N extends BaseGraphNode<S>,
C extends BaseGraphNodeConnection<S, N, N>,
SCHEMES extends BaseGraphSchemes<S, N, C>
>(
container: HTMLElement,
props: CreateGraphEditorProps<S, N, C, SCHEMES>
): Promise<GraphEditorContext<S, N, C, SCHEMES>> {
type AreaExtra = ReactArea2D<SCHEMES>;
const editor = new NodeEditor<SCHEMES>();
const area = new AreaPlugin<SCHEMES, AreaExtra>(container);
const connection = new ConnectionPlugin<SCHEMES, AreaExtra>();
const render = new ReactPlugin<SCHEMES, AreaExtra>({ createRoot });
AreaExtensions.selectableNodes(area, AreaExtensions.selector(), {
accumulating: AreaExtensions.accumulateOnCtrl(),
});
// Configure render
render.addPreset(
Presets.classic.setup({
customize: {
node(data) {
return ({ emit }) => {
return props.render?.node?.(data.payload, emit) ?? <Presets.classic.Node data={data.payload} emit={emit} />
}
},
connection(data) {
// const { source, target } = getConnectionSockets(editor, data.payload);
return () => {
return props.render?.connection?.(data.payload) ?? <Presets.classic.Connection data={data.payload} />
};
},
socket(data) {
return () => {
return props.render?.socket?.(data.payload as S) ?? <Presets.classic.Socket data={data.payload} />
}
}
},
})
);
// Configure connection
connection.addPreset(() => {
return new ClassicFlow({
canMakeConnection(from: SocketData, to: SocketData) {
const [source, target] = getSourceTarget(from, to) || [null, null];
if (!source || !target || from === to) return false;
const sourceNode = editor.getNode(source.nodeId)!;
const targetNode = editor.getNode(target.nodeId)!;
const sockets = getConnectionSockets(
editor,
props.connectionFactory(
sourceNode,
source.key,
targetNode,
target.key
)
);
if (!sockets.source!.isCompatibleWith(sockets.target!)) {
props.events?.onInvalidConnection?.();
connection.drop();
return false;
}
const connected = editor
.getConnections()
.find((conn) => conn.source == sourceNode.id &&
conn.target == targetNode.id &&
conn.sourceOutput == source.key &&
conn.targetInput == target.key
) != null;
// Already connected before
if (connected) {
connection.drop();
return false;
}
return Boolean(source && target);
},
makeConnection(from: SocketData, to: SocketData, context) {
const [source, target] = getSourceTarget(from, to) || [null, null];
const { editor } = context;
if (source && target) {
void editor.addConnection(
props.connectionFactory(
editor.getNode(source.nodeId)!,
source.key,
editor.getNode(target.nodeId)!,
target.key
)
);
return true;
}
}
});
});
editor.use(area);
area.use(connection);
area.use(render);
AreaExtensions.simpleNodesOrder(area);
return {
rete: {
editor: editor,
area: area,
connection: connection,
},
autoFitViewport: () => {
AreaExtensions.zoomAt(area, editor.getNodes())
},
destroy: () => area.destroy(),
};
}
为了适配 useRete 的参数类型:
export declare function useRete<T extends {
destroy(): void;
}>(create: (el: HTMLElement) => Promise<T>): readonly [React.RefObject<HTMLDivElement>, T | null];
因此需要再把上面的 createBaseGraphEditor 再包装一层:
export function useCreateReteBaseGraphEditor<
S extends BaseGraphSocket,
N extends BaseGraphNode<S>,
C extends BaseGraphNodeConnection<S, N, N>,
SCHEMES extends BaseGraphSchemes<S, N, C>
>(props: CreateGraphEditorProps<S, N, C, SCHEMES>) {
return useCallback(
(container: HTMLElement) => {
return createBaseGraphEditor(container, props);
},
[]
)
}
注意这里必须用 useCallback 来接管这个返回结果,否则会导致 Rete 无限重复渲染。
测试封装
到这里所有的封装就结束了,现在可以用刚才封装好的类和方法,重新实现一个快速开始中的例子了:
定义插槽
type MySocketTypes = ExecSocket | ParamSocket;
class ExecSocket extends BaseGraphSocket {
constructor() {
super("Exec");
}
isCompatibleWith(socket: ClassicPreset.Socket): boolean {
return socket instanceof ExecSocket;
}
}
class ParamSocket extends BaseGraphSocket {
constructor() {
super("Param");
}
isCompatibleWith(socket: ClassicPreset.Socket): boolean {
return socket instanceof ParamSocket;
}
}
// 注意下面的插槽必须是单例
const execSocket = new ExecSocket();
const paramSocket = new ParamSocket();
定义节点与 Schemes
class MyBaseGraphNode extends BaseGraphNode<MySocketTypes> {
constructor(nodeType: string) {
super(nodeType);
}
}
class MyBaseGraphConnection extends BaseGraphNodeConnection<MySocketTypes, MyBaseGraphNode, MyBaseGraphNode> {}
type MyGraphSchemes = BaseGraphSchemes<MySocketTypes, MyBaseGraphNode, MyBaseGraphConnection>
组件示例
function App() {
const [ref, ctx] = useRete(
useCreateReteBaseGraphEditor<
MySocketTypes,
MyBaseGraphNode,
MyBaseGraphConnection,
MyGraphSchemes
>({
connectionFactory: (
fromNode
, fromSocket,
toNode,
toSocket
) => {
return new MyBaseGraphConnection(fromNode, fromSocket, toNode, toSocket)
}
})
);
useEffect(() => {
if (!ctx) return
const node1 = new MyBaseGraphNode("NODE1")
node1.addOutputSocket("Exec", execSocket)
node1.addOutputSocket("x", paramSocket)
void ctx.rete.editor.addNode(node1)
const node2 = new MyBaseGraphNode("NODE2")
node2.addInputSocket("Exec", execSocket)
node2.addInputSocket("y", paramSocket)
void ctx.rete.editor.addNode(node2)
void ctx.rete.editor.addConnection(new MyBaseGraphConnection(node1, "Exec", node2, "Exec"))
void ctx.rete.editor.addConnection(new MyBaseGraphConnection(node1, "x", node2, "y"))
}, [ctx]);
return (
<>
<div>
<div ref={ref} style={{ height: "100vh", width: "100vw" }}></div>
</div>
</>
)
}
如果不出意外的话,你会看到 NODE1 和 NODE2 连在一起,并且不同类型的插槽不能连接。
结束
到这里 Rete.js 的基本使用就结束了,更多玩法可以在官方文档和示例中找到:https://retejs.org/examples


