Article
第1章:Rete.js 简介与核心概念
1.1 什么是 Rete.js
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| Rete.js | 一个基于 JavaScript 的可视化节点编辑器框架,用于构建可交互的流程图和规则引擎。它允许开发者通过拖拽节点和连接线的方式设计复杂的逻辑流程。 | Rete.js 不是完整的低代码平台,而是一个可扩展的底层框架,需结合插件和自定义逻辑使用。 |
| 开源协议 | MIT 许可证,允许自由使用、修改和分发,适用于商业项目。 | 使用时建议查看最新版本的 LICENSE 文件以确认条款。 |
| 核心目标 | 提供一套模块化、可扩展的 API,用于创建基于 Rete 算法的运行时节点网络。 | 主要关注逻辑执行层,UI 渲染需依赖额外的渲染插件(如 Vue 或 React 渲染器)。 |
| 框架定位 | 属于”节点编辑器引擎”,与 Node-RED、LogicFlow、Drawflow 类似,但更侧重于规则推理和高效匹配。 | 不直接提供图形界面组件,需集成 render plugin 实现可视化。 |
1.2 Rete 算法原理简介
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| Rete 算法起源 | 由 Charles L. Forgy 在 1979 年提出,用于高效匹配规则条件与事实数据。 | 原始算法用于专家系统和规则引擎(如 CLIPS、Drools),Rete.js 是其 JS 实现变体。 |
| 匹配-选择-执行循环(Match-Select-Act) | 规则引擎的基本运行机制: 1. 匹配:将规则条件与当前工作内存中的事实进行比对; 2. 选择:从满足条件的规则中选择一条执行; 3. 执行:运行选中规则的动作。 | Rete.js 默认不强制实现选择策略,开发者可自定义执行顺序。 |
| 工作内存(Working Memory) | 存储当前所有”事实”(facts)的地方,通常是输入数据或中间结果。 | 在 Rete.js 中,工作内存由 Engine 和节点间的数据传递模拟实现。 |
| 生产规则(Production Rules) | 形如”if 条件 then 动作”的逻辑表达式。每个节点可视为一个规则单元。 | Rete.js 将规则拆分为节点(Node)和连接(Connection),形成有向图结构。 |
| 网络结构(Rete Network) | 一种有向无环图(DAG),节点表示条件或操作,边表示数据流或控制流。 | Rete.js 构建的节点图本质上是对 Rete 网络的简化和泛化,支持动态修改。 |
| 节点类型(常见) | - Alpha 节点:过滤单个条件 - Beta 节点:组合多个条件 - 终端节点:触发动作 | Rete.js 抽象了这些类型,开发者通过自定义 Node 类实现对应逻辑。 |
| 增量更新机制 | 当事实变化时,仅重新计算受影响的路径,避免全量匹配,提升性能。 | Rete.js 利用连接依赖关系实现类似机制,适合频繁更新的场景。 |
1.3 Rete.js 的架构与组件概览
| 组件名称 | 说明 | 注意事项 |
|---|---|---|
| Editor(编辑器实例) | 核心容器,管理节点、连接、事件和插件的生命周期。 | 必须首先创建 Editor 实例,其他组件依附其上。 |
| Node(节点) | 表示一个处理单元,包含输入、输出、控制项和执行逻辑。 | 需继承 Rete.Node 或使用 nodeCreator 函数创建自定义节点。 |
| Input / Output | 节点的输入端口和输出端口,定义数据流入流出的通道。 | 类型需匹配才能连接,支持数据类型校验(如 number、string、any)。 |
| Connection(连接) | 表示两个端口之间的连线,传递数据或触发执行。 | 由 ConnectionPlugin 管理,不可跨类型连接(可通过 any 类型绕过)。 |
| Control(控件) | 嵌入节点内部的 UI 元素,如文本框、按钮等,用于配置节点参数。 | 不参与数据流,仅用于用户交互,需配合渲染器使用。 |
| Engine(执行引擎) | 负责从输入节点开始,按连接顺序执行节点逻辑,支持同步/异步执行。 | 必须注册所有节点类型,否则无法执行;可通过 engine.execute() 触发运行。 |
| Plugin(插件系统) | 模块化扩展机制,如渲染、连接校验、上下文菜单等均由插件提供。 | 插件需按顺序注册,部分插件有依赖关系(如 RenderPlugin 依赖 ConnectionPlugin)。 |
1.4 典型应用场景分析
| 应用场景 | 说明 | 注意事项 |
|---|---|---|
| 可视化编程工具 | 如 Blockly、Node-RED 替代方案,允许用户通过图形化方式编写程序逻辑。 | 适合教育、自动化脚本生成等场景,需设计丰富的节点库。 |
| 业务规则引擎 | 实现”如果订单金额 > 1000,则打 9 折”类逻辑,便于非技术人员配置规则。 | 结合后端持久化和版本管理,提升可维护性。 |
| 游戏对话系统 | 构建 NPC 对话树,支持分支、条件判断和剧情跳转。 | 可将每个对话节点作为 Rete 节点,通过条件输出控制流程走向。 |
| 数据处理流水线 | 定义 ETL(抽取、转换、加载)流程,如清洗数据 → 聚合 → 输出报表。 | 每个处理步骤为一个节点,支持并行和串行执行。 |
| AI 行为树(Behavior Tree) | 构建游戏角色的决策逻辑,如”巡逻 → 发现敌人 → 追击”。 | 需扩展节点类型以支持并行、选择、重复等行为树结构。 |
| 低代码/无代码平台 | 作为底层引擎支撑可视化逻辑编排功能,集成到更大系统中。 | 需配合表单设计器、API 管理等模块使用,注重用户体验和稳定性。 |
| 自动化测试流程编排 | 定义测试用例的执行顺序、条件分支和断言逻辑。 | 支持动态参数注入和结果反馈,便于调试和复用。 |
第2章:环境搭建与第一个节点编辑器
2.1 安装 Rete.js 及依赖
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| npm install | npm install rete | 安装 Rete.js 核心库 | npm install rete | 必须首先安装核心库,否则无法创建编辑器实例。 |
| yarn add | yarn add rete | 使用 Yarn 包管理器安装 Rete.js | yarn add rete | 功能与 npm 相同,适用于使用 Yarn 的项目。 |
| 安装渲染插件 | npm install rete-vue-render-plugin@next | 安装 Vue 3 渲染插件(推荐用于 Vue 项目) | npm install rete-vue-render-plugin@next | 若使用 Vue,必须安装对应版本的渲染插件;@next 表示最新实验版。 |
| 安装渲染插件 | npm install rete-react-render-plugin | 安装 React 渲染插件(用于 React 项目) | npm install rete-react-render-plugin | React 渲染插件支持函数组件和类组件集成。 |
| 安装连接插件 | npm install rete-connection-plugin | 安装连接管理插件,支持节点间的连线操作 | npm install rete-connection-plugin | 所有可视化编辑功能都需要此插件,否则无法创建连接。 |
| 安装控制插件 | npm install rete-control-plugin | 安装控制项插件,用于在节点中添加按钮、输入框等 UI 控件 | npm install rete-control-plugin | 非必需,但常用于配置节点参数。 |
2.2 创建基础编辑器实例
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Editor 构造函数 | new Editor() | 创建一个空的节点编辑器实例 | import { NodeEditor } from 'rete';const editor = new NodeEditor(); | 必须先创建 editor 实例,后续所有组件都注册到该实例上。 |
| register() | editor.register(nodeClass) | 注册自定义节点类型,使编辑器识别该节点 | editor.register(MyNode); | 每个自定义节点类必须通过 register 注册后才能使用。 |
| connect() | editor.connect(output, input) | 在两个端口之间建立连接 | await editor.connect(outPort, inPort); | 返回 Promise,需 await 等待连接完成;连接失败会抛出错误。 |
| nodes | editor.nodes | 获取当前编辑器中所有节点的数组 | const allNodes = editor.nodes; | 只读属性,返回节点实例列表,可用于遍历或调试。 |
| view | editor.view | 获取编辑器的视图对象(由渲染插件提供) | const { container } = editor.view; | 仅在渲染插件加载后可用,用于将编辑器挂载到 DOM 容器。 |
| addNode() | editor.addNode(nodeInstance) | 向编辑器添加一个节点实例 | await editor.addNode(new MyNode()); | 异步方法,需 await;节点必须已注册且构造完成。 |
| removeNode() | editor.removeNode(nodeInstance) | 从编辑器中移除指定节点 | await editor.removeNode(node); | 同时会自动断开相关连接。 |
| on() | editor.on(eventName, callback) | 监听编辑器事件,如 'process', 'nodecreate' 等 | editor.on('nodecreate', (node) => console.log(node)); | 支持多个事件类型,可用于调试或触发外部逻辑。 |
| trigger() | editor.trigger(eventData) | 手动触发一个事件 | editor.trigger({ type: 'customEvent', data: {} }); | 自定义事件可用于跨模块通信。 |
2.3 使用 Vue 或 React 集成 Rete.js(可选框架)
Vue 集成
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| VueRenderPlugin | import { VueRenderPlugin } from 'rete-vue-render-plugin';editor.use(VueRenderPlugin); | 将 Vue 渲染能力注入编辑器 | const render = VueRenderPlugin();editor.use(render); | 必须在调用 mount 前 use 插件;支持 Vue 3 Composition API。 |
| render.add(nodeType, component) | render.add(MyNode, MyNodeComponent); | 为特定节点类型绑定 Vue 组件 | // MyNodeComponent 是一个 defineComponent 定义的组件 | 组件需暴露 props 接收节点数据,并可通过 emit 与逻辑层通信。 |
| mount() | render.mount(container, editor); | 将编辑器渲染到指定 DOM 容器 | const container = document.getElementById('editor');render.mount(container, editor); | container 必须存在且可见;建议在 Vue 的 onMounted 钩子中调用。 |
React 集成
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| ReactRenderPlugin | import { ReactRenderPlugin } from 'rete-react-render-plugin';editor.use(ReactRenderPlugin); | 将 React 渲染能力注入编辑器 | const render = ReactRenderPlugin();editor.use(render); | 支持 React 16+,可在函数组件中使用。 |
| render.add(nodeType, component) | render.add(MyNode, MyNodeComponent); | 为特定节点类型绑定 React 函数组件 | // MyNodeComponent 是一个 (props) => JSX.Element 形式的组件 | props 包含 node、emit 等属性,可用于响应用户交互。 |
| render.render() | render.render(editor, container); | 渲染编辑器到指定 DOM 节点 | render.render(editor, document.getElementById('editor')); | 需确保 container 已挂载到 DOM;通常在 useEffect 中执行。 |
| render.destroy() | render.destroy(); | 销毁渲染实例,清理事件监听和 DOM 元素 | useEffect(() => { return () => render.destroy(); }, []); | 在组件卸载时调用,防止内存泄漏。 |
第3章:核心模块详解
3.1 Engine 模块:节点执行与控制流
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Engine 构造函数 | new Engine(processors) | 创建一个执行引擎实例,可传入处理器映射表 | import { Engine } from 'rete';const engine = new Engine(); | 必须创建 Engine 实例来执行节点逻辑,通常与编辑器配对使用。 |
| register() | engine.register(nodeClass) | 注册节点类,使引擎知道如何执行该类型节点 | engine.register(MyNode); | 所有参与执行的节点类型都必须注册,否则 execute 会跳过。 |
| process() | engine.process(data) | 向引擎输入初始数据,触发从输入节点开始的执行流程 | await engine.process({ input: 10 }); | 异步方法,返回 Promise;可用于传递全局上下文或触发信号。 |
| execute() | engine.execute(fromNode, input, data) | 从指定节点的输入端口开始执行,传递数据 | await engine.execute(node, 'exec', value); | 用于控制流触发;fromNode 为节点实例,input 为输入名,data 为传递值。 |
| async execute() | engine.execute(node, 'data', value) | 异步执行支持,节点可返回 Promise | async execute(input, data) { return data * 2; } | 支持 await 处理异步逻辑,如 API 调用、延时等。 |
| remove() | engine.remove(nodeClass) | 从引擎中移除某类节点的注册 | engine.remove(MyNode); | 移除后该类型节点将不再被执行。 |
| processors | engine.processors | 获取当前注册的所有处理器(节点执行逻辑) | const processors = engine.processors; | 只读属性,可用于调试或动态修改执行逻辑。 |
3.2 Node 模块:自定义节点类
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Node 构造函数 | class MyNode extends Rete.Node { } | 定义一个自定义节点类 | class AddNode extends Rete.Node { constructor() { super('Add'); } } | 必须继承 Rete.Node,构造函数中调用 super 并传入节点名称。 |
| constructor() | constructor() { super(name); } | 初始化节点,设置节点名称 | super('CustomNode'); | name 将显示在编辑器中,建议唯一且语义化。 |
| builder() | async builder(node) | 构建节点结构:添加输入、输出、控制项 | builder(node) { node.addInput(new Input('num', 'Number')); } | 异步方法,可动态加载配置;所有端口和控件在此方法中定义。 |
| worker() | async worker(node, inputs, outputs) | 节点执行时调用的工作函数,处理数据逻辑 | worker(node, inputs) { return inputs['num'] * 2; } | 必须定义,否则节点无执行逻辑;inputs 是输入数据对象,outputs 是输出对象。 |
| id | node.id | 节点实例的唯一标识符 | const nodeId = node.id; | 由编辑器自动生成,全局唯一,可用于序列化或调试。 |
| name | node.name | 节点类型名称(来自构造函数) | const typeName = node.name; | 用于区分不同类型的节点。 |
| addInput() | node.addInput(inputInstance) | 向节点添加一个输入端口 | this.addInput(new Rete.Input('value', 'Value')); | 必须在 builder 中调用。 |
| addOutput() | node.addOutput(outputInstance) | 向节点添加一个输出端口 | this.addOutput(new Rete.Output('result', 'Result')); | 支持多个输出,用于分支逻辑。 |
| addControl() | node.addControl(controlInstance) | 向节点添加一个 UI 控件 | this.addControl(new Rete.Control('text')); | 控件不参与数据流,仅用于用户配置。 |
3.3 Input / Output 模块:数据输入输出定义
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Input 构造函数 | new Input(key, name, socket, multiple) | 创建一个输入端口 | new Input('in', 'Input', socket, false); | key 用于数据访问,name 显示在 UI,socket 定义类型,multiple 是否允许多连接。 |
| Output 构造函数 | new Output(key, name, socket, multiple) | 创建一个输出端口 | new Output('out', 'Output', socket, true); | multiple=true 时可连接多个下游节点。 |
| key | input.key / output.key | 端口的唯一键名,用于 worker 中访问数据 | inputs['value'] // 使用 key 获取输入值 | 必须唯一且符合变量命名规范。 |
| name | input.name / output.name | 显示在 UI 上的端口标签 | new Input('val', 'Value', socket); // UI 显示 "Value" | 可本地化,不影响逻辑。 |
| socket | input.socket / output.socket | 定义端口的数据类型和连接规则 | const numSocket = new Rete.Socket('Number');new Input('n', 'Num', numSocket); | 相同 socket 类型的端口才能连接。 |
| multiple | input.multiple / output.multiple | 是否允许多个连接 | new Output('data', 'Data', socket, true); | multiple=false 时新连接会替换旧连接。 |
| isInput | input.isInput | 判断是否为输入端口(只读) | if (port.isInput) { ... } | 用于通用逻辑处理。 |
| isOutput | output.isOutput | 判断是否为输出端口(只读) | if (port.isOutput) { ... } | 用于遍历和校验。 |
3.4 Connection 模块:连接逻辑与校验
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Connection 构造函数 | new Connection(from, output, to, input) | 创建两个节点端口之间的连接 | new Connection(node1, 'out', node2, 'in'); | from 和 to 为节点实例,output 和 input 为端口 key。 |
| from | connection.from | 源节点实例 | const sourceNode = connection.from; | 只读属性。 |
| to | connection.to | 目标节点实例 | const targetNode = connection.to; | 只读属性。 |
| output | connection.output | 源节点的输出端口 key | const outKey = connection.output; | 用于定位数据来源。 |
| input | connection.input | 目标节点的输入端口 key | const inKey = connection.input; | 用于定位数据去向。 |
| connect() | editor.connect(from, output, to, input) | 通过编辑器创建连接(推荐方式) | await editor.connect(node1, 'out', node2, 'in'); | 返回 Promise,自动触发渲染更新。 |
| disconnect() | editor.removeConnection(connection) | 移除指定连接 | await editor.removeConnection(conn); | 同时从编辑器中删除该连接实例。 |
| validator | connection.validator = (connection) => {} | 自定义连接校验函数,决定是否允许连接 | conn.validator = (c) => c.from.name !== c.to.name; | 返回 true 允许,false 禁止;可在 builder 或插件中设置。 |
3.5 Control 模块:UI 控件集成(如按钮、文本框)
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Control 构造函数 | new Control(component, props, emitters) | 创建一个嵌入节点的 UI 控件 | new Control('input', { value: 'hello' }); | component 可为字符串(内置)或组件对象,props 传递初始值。 |
| addComponent() | control.addComponent(name, component) | 注册一个可复用的控件组件 | Control.addComponent('my-button', MyButton); | 全局注册,可在多个节点中使用。 |
| setValue() | control.setValue(value) | 更新控件的值,并触发 change 事件 | control.setValue(42); | 通常用于外部逻辑更新控件状态。 |
| getValue() | control.getValue() | 获取控件当前值 | const val = control.getValue(); | 用于在 worker 中读取用户配置。 |
| emit() | control.emit(event) | 触发控件事件,通知节点或其他模块 | control.emit('click'); | 可自定义事件类型,常用于按钮点击等交互。 |
| component | control.component | 获取绑定的 UI 组件 | const comp = control.component; | 可用于动态切换控件类型。 |
| props | control.props | 获取传递给控件的属性 | const { value } = control.props; | 只读,初始化时设置。 |
| emitters | control.emitters | 获取事件发射器映射表 | control.emitters['change'] = (val) => {...}; | 用于监听控件内部事件,如输入框 change。 |
第4章:视图层与渲染器(Renderer)
4.1 使用 Rete.js 默认渲染器(VueRenderPlugin / ReactRenderPlugin)
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| VueRenderPlugin | import { VueRenderPlugin } from 'rete-vue-render-plugin'; | 创建 Vue 渲染插件实例 | editor.use(VueRenderPlugin()); | 必须在 editor.use() 中注册,且在 mount 前完成。 |
const render = VueRenderPlugin(); | ||||
| ReactRenderPlugin | import { ReactRenderPlugin } from 'rete-react-render-plugin'; | 创建 React 渲染插件实例 | editor.use(ReactRenderPlugin()); | 支持 React 16+,需确保 React 和 ReactDOM 已安装。 |
const render = ReactRenderPlugin(); | ||||
| render.add() | render.add(NodeClass, Component) | 为指定节点类型绑定自定义 UI 组件 | render.add(NumberNode, NumberNodeView); | Component 为 Vue 或 React 组件,接收 node、emit 等 props。 |
| render.mount() | render.mount(container, editor) | 将编辑器挂载到 DOM 容器(Vue) | const container = document.getElementById('editor');render.mount(container, editor); | container 必须存在;Vue 项目中通常在 onMounted 钩子调用。 |
| render.render() | render.render(editor, container) | 渲染编辑器到指定容器(React) | render.render(editor, document.getElementById('editor')); | React 项目使用此方法,替代 mount。 |
| render.destroy() | render.destroy() | 销毁渲染实例,清理事件和 DOM | render.destroy(); | 组件卸载时必须调用,防止内存泄漏。 |
| use() | editor.use(plugin, options) | 将渲染插件注册到编辑器 | editor.use(VueRenderPlugin({ customize: {} })); | options 可用于定制渲染行为。 |
4.2 自定义节点 UI 样式与布局
| 概念/方法名称 | 说明 | 代码示例 | 注意事项 |
|---|---|---|---|
| 节点 CSS 类名 | 渲染器为节点容器添加默认类名 .node,类型名如 .number-node | .node { background: #fff; border: 1px solid #ccc; }.add-node { width: 120px; } | 可通过全局 CSS 定制样式,建议使用 BEM 命名避免冲突。 |
| 端口类名 | 输入端口:.input,输出端口:.output,控制项:.control | .input { color: blue; }.output .port { background: green; } | 端口元素可通过类名精确定位,用于调整位置或颜色。 |
| 自定义组件结构 | 在 Vue/React 组件中完全控制节点 HTML 结构 | Vue: 模板语法 + defineComponentReact: JSX + 函数组件 | 可实现复杂布局,如多行文本、图标、状态指示灯等。 |
| 动态类名绑定 | 根据节点状态动态添加 CSS 类 | :class="{ 'active': node.selected }" | 用于高亮选中节点、错误状态提示等交互反馈。 |
| 内联样式 | 使用 style 属性直接设置样式 | style={{ width: '120px' }} | 适用于动态尺寸或位置调整,但优先级高于外部 CSS。 |
| 布局建议 | 推荐使用 Flexbox 或 Grid 布局确保端口对齐 | display: flex; justify-content: space-between; | 避免绝对定位导致响应式问题。 |
| 响应式设计 | 使用相对单位(em, rem, %)和媒体查询 | @media (max-width: 768px) { .node { font-size: 12px; } } | 适配移动端或小屏幕设备。 |
4.3 事件绑定与用户交互处理
| 方法名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| emit() | control.emit('event', data) | 从控件触发事件,通知节点或引擎 | control.emit('update', newValue); | 常用于输入框 change、按钮 click 等场景。 |
| on() | editor.on('nodeselected', callback) | 监听编辑器级事件 | editor.on('nodeselected', ({ node }) => console.log(node)); | 支持事件:'nodeselected', 'nodedeselected', 'connectioncreated' 等。 |
| node.selected | node.selected | 判断节点是否被选中 | if (node.selected) { ... } | 可用于 UI 高亮或条件操作。 |
| handle click | @click="() => emit('click')" | 在 Vue 组件中绑定点击事件 | <button @click="() => emit('click')">执行</button> | emit 第二个参数可传递数据。 |
| handle input change | @input="e => emit('input', e.target.value)" | 监听输入框变化并发送值 | <input @input="e => emit('input', e.target.value)" /> | 推荐使用 input 事件而非 change,响应更及时。 |
| drag start / end | editor.on('dragstart', handler) | 监听拖拽节点开始和结束 | editor.on('dragstart', () => lockOtherUI());editor.on('dragend', unlockUI); | 可用于禁用其他操作避免冲突。 |
| contextmenu | editor.on('contextmenu', handler) | 处理右键菜单事件 | editor.on('contextmenu', (e) => showMenu(e.x, e.y)); | 需阻止默认行为 e.preventDefault()。 |
| doubleClick | editor.on('nodedoubleclicked', handler) | 监听节点双击事件 | editor.on('nodedoubleclicked', ({ node }) => openConfigPanel(node)); | 适合打开配置弹窗或进入编辑模式。 |
第5章:数据流与执行引擎
5.1 数据流动机制(Dataflow)
| 概念/机制名称 | 说明 | 代码示例或逻辑描述 | 注意事项 |
|---|---|---|---|
| 推模式 (Push) | 数据由上游节点输出后,主动”推送”给下游节点触发执行 | 当节点 A 输出数据,连接的节点 B 自动调用其 worker 方法 | Rete.js 默认采用推模式,适合事件驱动和实时响应场景。 |
| 拉模式 (Pull) | 下游节点在需要时主动”拉取”上游节点的数据 | 节点 B 在 worker 中显式调用 getInputData('inputName') 获取数据 | 需自定义实现,适合按需计算或性能优化场景。 |
| 数据触发 | 连接建立后,上游节点的输出数据会自动触发下游节点的输入 | 连接 A.out -> B.in 后,A 执行完输出数据,B 立即开始执行 | 触发依赖于连接和端口类型匹配。 |
| 数据缓存 | 节点可缓存上一次的输出结果,避免重复计算 | node.cache = true; | 在 worker 中可通过 this.cache 访问缓存,优化性能。 |
| 端口键(key) | 每个输入/输出端口有唯一 key,用于数据传递和 worker 中访问 | inputs['value'] 获取 key 为 'value' 的输入数据 | key 必须与连接时定义的 key 一致。 |
| Socket 类型匹配 | 只有相同或兼容 Socket 类型的端口才能连接,保证数据类型安全 | new Socket('Number') 的输出只能连接到同类型输入 | 是数据流校验的核心机制,防止类型错误。 |
| 多输入合并 | 一个节点可有多个输入,数据以对象形式传入 worker | worker(node, inputs) { const a = inputs['in1']; const b = inputs['in2']; } | 输入数据按 key 组织,未连接的输入为 null 或 undefined。 |
5.2 使用 Engine 执行节点逻辑
| 方法/属性名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Engine 实例化 | const engine = new Engine(); | 创建执行引擎 | import { Engine } from 'rete';const engine = new Engine(); | 必须先创建引擎,并与编辑器关联。 |
| register() | engine.register(NodeClass) | 注册节点类,使引擎知道如何执行该节点 | engine.register(AddNode); | 所有参与执行的节点类型都必须注册,否则 execute 会跳过。 |
| process() | await engine.process(data) | 启动执行流程,向引擎注入初始数据 | await engine.process({ start: true }); | 异步方法,常用于触发整个流程或传递全局参数。 |
| execute() | await engine.execute(node, input, data) | 从指定节点的指定输入端口开始执行,传递数据 | await engine.execute(node, 'exec', 100); | node 是节点实例,input 是输入端口 key,data 是传递的值。 |
| remove() | engine.remove(NodeClass) | 从引擎中移除某类节点的注册 | engine.remove(CustomNode); | 移除后该类型节点不再被执行。 |
| processors | engine.processors | 获取当前注册的所有处理器(节点执行逻辑) | console.log(engine.processors); | 只读属性,可用于调试或动态管理。 |
| worker 执行流程 | async worker(node, inputs, outputs) | 节点的工作函数,处理输入、执行逻辑、返回输出 | worker(node, inputs) { return { result: inputs['num'] * 2 }; } | 必须返回一个对象,键为输出端口 key,值为输出数据。 |
| 错误处理 | try { await engine.process() } catch(e) | 捕获执行过程中的异常 | try { await engine.process(); } catch (err) { console.error(err); } | 节点 worker 抛出异常会中断执行流,需妥善处理。 |
5.3 异步节点支持与 Promise 处理
| 特性/方法名称 | 说明 | 代码示例 | 注意事项 |
|---|---|---|---|
| async worker | worker 方法支持 async,可返回 Promise | async worker(node, inputs) {const res = await fetch('/api/data');return { result: await res.json() };} | 引擎会自动 await worker 的返回值,支持异步操作。 |
| Promise 返回 | worker 可直接返回 Promise 对象 | worker() { return Promise.resolve({ result: 42 }); } | 与 async/await 效果相同,提供更灵活的异步控制。 |
| 并行执行 | 多个异步节点可并行执行,互不阻塞 | 节点 A 和 B 无依赖关系时,engine 会同时触发其 worker | 提高执行效率,特别适合 I/O 密集型任务。 |
| 顺序执行 | 通过连接控制执行顺序,前一个异步节点完成后再触发下一个 | A.out -> B.in,B 在 A 的 worker 完成后才开始执行 | 保证数据依赖的正确性。 |
| 错误传播 | 异步 worker 中 reject 或抛出异常,会中断后续执行流 | async worker() { throw new Error('API failed'); } | 需在 process() 或 execute() 外层 try/catch 捕获。 |
| 超时处理 | 可结合 Promise.race() 或 AbortController 实现超时 | const timeout = new Promise((_, r) => setTimeout(() => r('timeout'), 5000));return Promise.race([fetchCall, timeout]); | 防止异步操作无限等待,提升系统健壮性。 |
| 状态反馈 | 异步执行期间,可通过 control.emit() 更新 UI 状态(如加载中) | control.emit('loading', true);const data = await apiCall();control.emit('loading', false); | 提升用户体验,让用户知晓执行进度。 |
第6章:高级功能扩展
6.1 插件系统介绍(Plugin System)
Rete.js 采用模块化插件架构,允许功能解耦和按需扩展。插件通过 editor.use(plugin) 注册,可访问编辑器实例并扩展其功能。
| 概念/机制名称 | 说明 | 代码示例或逻辑描述 | 注意事项 |
|---|---|---|---|
| 插件定义 | 插件是一个对象或函数,包含 name、install 方法或直接为 install 函数 | function MyPlugin() { return { name: 'MyPlugin', install: (editor) => { ... } }; } | 推荐使用对象形式,便于调试和管理。 |
| install 方法 | 插件注册时自动调用,接收 editor 实例作为参数 | install(editor) { editor.on('process', () => console.log('Processing...')); } | 可在此方法中监听事件、扩展 API、注册子插件。 |
| editor.use() | 注册插件的核心方法 | editor.use(MyPlugin()); | 必须在编辑器初始化后、挂载前调用。 |
| 插件依赖 | 插件可声明对其他插件的依赖,确保加载顺序 | 在 install 中检查所需 API 是否存在 | 例如:ConnectionPlugin 必须在 VueRenderPlugin 前注册。 |
| 插件通信 | 插件间通过 editor.emit() 和 editor.on() 事件机制通信 | editor.emit('custom-event', data);editor.on('custom-event', handler); | 避免直接访问其他插件内部状态,保持松耦合。 |
| 生命周期管理 | 插件应提供 destroy 方法清理资源(如事件监听) | install(editor) { const handler = () => {}; editor.on('event', handler); return () => editor.off('event', handler); } | 防止内存泄漏,组件卸载时调用。 |
6.2 常用插件详解
| 插件名称 | 用途 | 安装与使用示例 | 核心功能与注意事项 |
|---|---|---|---|
| ConnectionPlugin | 提供节点间连接的创建、删除、校验等核心功能 | import { ConnectionPlugin } from 'rete-connection-plugin';editor.use(ConnectionPlugin()); | - 必须插件,无它则无法连接节点。 - 支持自定义连接样式和校验逻辑。 |
| ContextMenuPlugin | 为编辑器添加右键上下文菜单,支持自定义菜单项 | import { ContextMenuPlugin } from 'rete-context-menu-plugin';editor.use(ContextMenuPlugin({ items: [...] })); | - items 数组定义菜单项(文本、点击回调)。 - 支持动态生成菜单。 |
| MinimapPlugin | 添加小地图视图,便于在大型画布中导航 | import { MinimapPlugin } from 'rete-minimap-plugin';editor.use(MinimapPlugin()); | - 提供缩略图和视口框。 - 可拖拽视口框快速定位。 - 占用额外 DOM。 |
| AreaPlugin | 管理画布区域(缩放、平移、拖拽) | import { AreaPlugin } from 'rete-area-plugin';const area = AreaPlugin();editor.use(area); | - 提供 zoom, translate 方法。 - 处理鼠标/触摸事件。 |
| AutoArrangePlugin | 自动排列节点,优化布局 | import { AutoArrangePlugin } from 'rete-auto-arrange-plugin';editor.use(AutoArrangePlugin()); | - 基于 DAG(有向无环图)算法布局。 - 可通过快捷键触发。 |
| Vue/Konva RenderPlugin | 分别提供 Vue/React 或 Canvas 渲染能力 | import { VueRenderPlugin } from 'rete-vue-render-plugin';editor.use(VueRenderPlugin()); | - Vue/React:组件化 UI,易集成。 - Konva:高性能,适合复杂图形。 |
| InspectorPlugin | 提供节点属性检查器面板,集中编辑选中节点的控件 | import { InspectorPlugin } from 'rete-inspector-plugin';editor.use(InspectorPlugin()); | - 类似 Unity Inspector。 - 提升大批量配置效率。 |
6.3 自定义插件开发
| 开发步骤 | 说明 | 代码示例 | 注意事项 |
|---|---|---|---|
| 1. 定义插件结构 | 创建函数或对象,返回包含 install 方法的对象 | function MyCustomPlugin(options = {}) {return {name: 'MyCustomPlugin',install: (editor) => {// 扩展逻辑}};} | name 用于调试和去重。 |
| 2. 监听编辑器事件 | 在 install 中使用 editor.on() 监听关键事件 | editor.on('nodeselected', ({ node }) => {console.log('Selected:', node);}); | 可监听:'nodecreated', 'noderemoved', 'connectioncreated' 等。 |
| 3. 扩展编辑器 API | 向 editor 添加自定义方法 | editor.exportToJson = () => {return { nodes: editor.nodes.toArray() };}; | 方法可被其他插件或业务代码调用。 |
| 4. 集成 UI 组件 | 结合渲染插件,在 DOM 中添加自定义 UI(如按钮、面板) | const button = document.createElement('button');button.innerText = 'Export';button.onclick = () => editor.emit('export');container.appendChild(button); | 注意挂载和销毁时机,避免内存泄漏。 |
| 5. 提供配置选项 | 通过 options 参数定制插件行为 | install(editor) {const { autoSaveInterval = 5000 } = options;setInterval(() => editor.emit('autosave'), autoSaveInterval);} | 增强插件灵活性和复用性。 |
| 6. 销毁资源 | 返回一个清理函数,由编辑器在销毁时调用 | let timer;timer = setInterval(...);return () => clearInterval(timer); | 确保所有事件监听、定时器、DOM 元素被正确清理。 |
| 7. 发布事件 | 使用 editor.emit() 通知其他模块 | editor.emit('dataProcessed', { result: data }); | 促进插件间协作,实现松耦合架构。 |
完整自定义插件示例:
function AutoSavePlugin(options = {}) {
const { interval = 30000 } = options;
let timer;
return {
name: 'AutoSavePlugin',
install(editor) {
timer = setInterval(() => {
const json = editor.toJSON(); // 假设有导出方法
localStorage.setItem('rete-autosave', JSON.stringify(json));
editor.emit('autosave', json);
}, interval);
// 返回销毁函数
return () => {
if (timer) clearInterval(timer);
};
}
};
}
// 使用
editor.use(AutoSavePlugin({ interval: 60000 }));
第7章:序列化与持久化
7.1 导出编辑器数据(JSON 序列化)
| 方法/属性名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| toJSON() | editor.toJSON() | 将当前编辑器状态(节点、连接等)序列化为 JSON 对象 | const data = editor.toJSON();console.log(data); | 包含 nodes 和 connections 数组,是标准导出方法。 |
| 节点数据结构 | { id, label, x, y, inputs, outputs, controls } | 节点在 JSON 中的表示 | id: "node-1", label: "Add", x: 100, y: 200 | x, y 为画布坐标;inputs/outputs 包含端口 key、连接状态等。 |
| 连接数据结构 | { id, source, sourceOutput, target, targetInput } | 连接在 JSON 中的表示 | source: "node-1", sourceOutput: "result", target: "node-2", targetInput: "num" | 明确指定源节点输出和目标节点输入的 key。 |
| 自定义字段序列化 | 在节点类中添加可序列化字段 | 将节点内部状态(如用户配置)包含在导出数据中 | class CustomNode extends Node {constructor() {super('Custom');this.customValue = 'default';}} | 需确保字段为基本类型(string, number, boolean, array, object)。 |
| engine.toJSON() | engine.toJSON() | (部分实现)导出引擎相关配置或状态 | const engineData = engine.toJSON(); | 并非所有引擎都支持,需查看具体插件文档。 |
| 手动过滤/处理 | JSON.stringify(data, replacer) | 自定义序列化过程,过滤敏感字段或转换数据 | const safeData = JSON.stringify(editor.toJSON(), (k, v) => k === 'password' ? undefined : v); | 使用 replacer 函数增强安全性或兼容性。 |
7.2 加载已有流程图(反序列化)
| 方法/步骤 | 说明 | 代码示例 | 注意事项 |
|---|---|---|---|
| fromJSON() | 将 JSON 数据加载回编辑器 | await editor.fromJSON(data); | 异步方法,需 await 等待完成。 |
| 注册节点类 | 加载前必须确保所有用到的节点类已注册 | editor.register(CustomNode);editor.register(AddNode); | 否则对应节点无法创建,导致连接丢失或错误。 |
| 异步加载顺序 | 先注册节点,再调用 fromJSON() | editor.use(ConnectionPlugin());editor.register(MyNode);await editor.fromJSON(savedData); | 顺序错误会导致数据无法正确解析。 |
| 处理连接丢失 | 检查反序列化后是否有未连接的端口 | editor.nodes.forEach(node => {node.inputs.forEach(input => {if (!input.hasConnection()) console.warn('Unconnected input:', input);});}); | 可能因节点类未注册或 Socket 类型不匹配导致。 |
| 恢复 UI 状态 | 坐标、选中状态等通常由渲染器管理 | render.render(editor, container); | fromJSON() 主要恢复逻辑结构,UI 渲染需后续触发。 |
| 错误处理 | 捕获 fromJSON() 可能抛出的异常 | try { await editor.fromJSON(data); } catch (err) { console.error('Load failed:', err); } | 数据格式错误或缺失依赖时会失败。 |
| 版本兼容性 | 处理不同版本间的数据结构变化 | 在 JSON 中添加 version 字段,并在加载时做迁移处理 | 如 if (data.version < 2) migrateToV2(data); |
7.3 数据结构设计最佳实践
| 最佳实践 | 说明 | 示例/建议 | 优势 |
|---|---|---|---|
| 使用语义化 Key | 为输入/输出端口使用清晰、有意义的 key | inputs.add('value', new NumberSocket()); 而非 inputs.add('in1', ...) | 提高可读性和维护性,便于调试。 |
| 模块化节点设计 | 单个节点职责单一,避免过于复杂 | 将”HTTP 请求 + JSON 解析 + 数据存储”拆分为三个节点 | 提高复用性,降低耦合,易于测试和替换。 |
| 标准化 Socket 类型 | 定义统一的 Socket 类型体系,避免随意创建 | const NumberSocket = new Socket('Number');const StringSocket = new Socket('String'); | 保证连接安全,减少类型错误。 |
| 包含元数据 | 在 JSON 中添加版本、作者、描述等元信息 | { version: "1.0", author: "dev", description: "Calc flow", nodes: [...] } | 便于管理和追溯,支持向后兼容。 |
| 控制数据扁平化 | 控件(Control)的状态尽量保持为简单值 | this.controls.add('text', new InputControl('text')); // value is string | 简化序列化,避免嵌套对象难以处理。 |
| 避免循环依赖 | 设计节点连接时防止形成闭环,导致执行死锁 | 使用 DAG(有向无环图)校验工具 | 保证数据流可执行,避免无限递归。 |
| 预留扩展字段 | 在节点或连接数据中预留 metadata 字段用于未来扩展 | node.metadata = { color: 'red', group: 'math' }; | 无需修改核心结构即可增加新功能(如分组着色)。 |
| 轻量级存储 | 仅序列化必要数据,避免冗余 | 不存储临时 UI 状态(如 hover),只存核心逻辑和配置 | 减少存储空间和传输开销,提升性能。 |
| 加密敏感数据 | 对密码、密钥等敏感信息进行加密后再存储 | control.value = encrypt(password); | 提升数据安全性,符合合规要求。 |
第8章:实战项目:构建一个可视化逻辑编辑器
8.1 需求分析与模块划分
| 模块/功能 | 需求描述 | 技术选型与实现方案 | 注意事项 |
|---|---|---|---|
| 核心编辑功能 | 创建、连接、删除节点,拖拽画布,缩放 | Rete.js 核心 + AreaPlugin(缩放/平移) + ConnectionPlugin(连接) | 确保基础交互流畅,支持鼠标/触摸操作。 |
| 节点类型 | 支持逻辑判断、数值计算、文本处理、数据输入/输出等基本节点 | 设计 ConditionNode, MathNode, TextNode, InputNode, OutputNode 等 | 节点职责单一,便于组合复用。 |
| UI 框架集成 | 使用 Vue 3 开发前端界面 | rete-vue-render-plugin + Vue 3 Composition API | 注意 Vue 3 的响应式机制与 Rete 节点状态同步。 |
| 上下文菜单 | 右键点击画布或节点,提供”添加节点”、“删除”、“复制”等操作 | ContextMenuPlugin 自定义菜单项 | 菜单项应根据上下文(空白处/节点上)动态变化。 |
| 小地图导航 | 大型流程图中快速定位 | MinimapPlugin | 可选功能,提升用户体验。 |
| 执行引擎 | 能运行构建的逻辑流程,支持同步/异步节点 | Rete.Engine + 自定义 worker 逻辑 | 异步节点需返回 Promise。 |
| 调试功能 | 显示节点执行状态(运行中、成功、失败),查看数据流 | 自定义 control.emit() 传递状态,在 UI 中高亮反馈 | 可结合日志面板输出执行信息。 |
| 保存/加载 | 将流程图保存为 JSON,支持从本地或服务器加载 | editor.toJSON() / editor.fromJSON() + localStorage 或 fetch API | 加载前必须注册所有节点类。 |
| 数据校验 | 连接时校验类型匹配,防止非法连接 | 自定义 ConnectionPlugin 的 validator 函数 | 如 Number 输出不能连接到 String 输入。 |
8.2 节点设计与注册
1. 基础节点类结构(以 MathNode 为例):
import { Node, Socket } from 'rete';
class MathNode extends Node {
constructor() {
super("MathOperation");
// 输入端口
const numSocket = new Socket('Number');
this.addInput('num1', numSocket);
this.addInput('num2', numSocket);
// 输出端口
this.addOutput('result', numSocket);
// 控件(UI 输入)
this.controls.add('operation', new SelectControl('Operation', ['+', '-', '*', '/']));
}
// 执行逻辑
async worker(node, inputs, outputs) {
const a = inputs['num1'] || 0;
const b = inputs['num2'] || 0;
const op = node.controls.get('operation').value;
let result;
switch (op) {
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
case '/': result = b !== 0 ? a / b : 0; break;
default: result = a + b;
}
return { result };
}
}
2. 注册所有节点:
// 必须在 editor.fromJSON() 前完成注册
editor.register(MathNode);
editor.register(ConditionNode);
editor.register(InputNode);
editor.register(OutputNode);
// ... 其他节点
3. 自定义 UI 组件(Vue 3):
<!-- MathNodeView.vue -->
<template>
<div class="math-node">
<h4>数学运算</h4>
<select :value="node.controls.get('operation').value" @change="onChange">
<option value="+">+</option>
<option value="-">-</option>
<option value="*">*</option>
<option value="/">/</option>
</select>
<!-- 端口由渲染器自动渲染 -->
</div>
</template>
<script>
export default {
props: ['node', 'emit'],
methods: {
onChange(e) {
this.node.controls.get('operation').setValue(e.target.value);
this.emit('update'); // 通知引擎可能需要重新执行
}
}
}
</script>
4. 绑定自定义 UI:
import { VueRenderPlugin } from 'rete-vue-render-plugin';
import MathNodeView from './MathNodeView.vue';
const render = VueRenderPlugin();
render.add(MathNode, MathNodeView); // 关联节点类与 UI 组件
8.3 流程执行与调试
1. 初始化并使用引擎:
import { Engine } from 'rete';
const engine = new Engine();
// 注册节点到引擎(必须与编辑器注册一致)
engine.register(MathNode);
engine.register(ConditionNode);
// ...
// 将引擎与编辑器关联(某些插件需要)
// editor.use(enginePlugin); // 取决于具体实现
2. 执行整个流程:
async function runFlow() {
try {
// 清除上次执行状态(如高亮)
clearExecutionHighlights();
// 启动执行
await engine.process(); // 或 engine.execute(startNode, 'exec', data)
console.log('流程执行完成');
} catch (err) {
console.error('执行出错:', err);
showErrorDialog(err.message);
}
}
3. 实现调试反馈(在节点 UI 中):
// 在节点 worker 开始时发送事件
async worker(node, inputs, outputs) {
node.controls.get('status').setValue('running'); // 假设有状态控件
emit('status', 'running');
// ... 执行逻辑
node.controls.get('status').setValue('success');
emit('status', 'success');
return { result };
}
// 在 UI 组件中监听
mounted() {
this.node.on('status', (status) => {
this.highlightColor = status === 'running' ? 'yellow' :
status === 'success' ? 'green' : 'red';
});
}
8.4 保存与加载功能实现
1. 保存流程图:
function saveFlow() {
const data = editor.toJSON(); // 获取 JSON 数据
const jsonStr = JSON.stringify(data, null, 2); // 格式化
// 方案1:下载到本地
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'flow.json';
a.click();
// 方案2:保存到 localStorage
localStorage.setItem('saved-flow', jsonStr);
// 方案3:上传到服务器
// fetch('/api/save', { method: 'POST', body: jsonStr });
}
2. 加载流程图:
async function loadFlow(jsonData) {
try {
// 确保所有节点类已注册(关键!)
registerAllNodes(editor); // 封装了所有 editor.register() 调用
await editor.fromJSON(jsonData);
console.log('流程图加载成功');
// 重新渲染(如果需要)
// render.render(editor, container);
} catch (err) {
console.error('加载失败:', err);
alert('加载失败:' + err.message);
}
}
// 从文件加载
function loadFromFile(file) {
const reader = new FileReader();
reader.onload = async (e) => {
const data = JSON.parse(e.target.result);
await loadFlow(data);
};
reader.readAsText(file);
}
// 从 localStorage 加载
function loadFromStorage() {
const saved = localStorage.getItem('saved-flow');
if (saved) {
loadFlow(JSON.parse(saved));
}
}
3. 初始化时尝试自动加载:
// 页面加载后
async function initEditor() {
// ... 创建 editor, 注册插件
// 尝试从 localStorage 恢复
const autoLoad = localStorage.getItem('auto-load-on-start');
if (autoLoad === 'true') {
await loadFromStorage();
}
}
第9章:性能优化与常见问题
9.1 大型流程图性能调优
| 优化策略 | 说明 | 实现方法与代码示例 | 注意事项 |
|---|---|---|---|
| 虚拟化渲染 (Virtualization) | 只渲染视口内的节点,减少 DOM 节点数量 | 使用 rete-vue-render-plugin 的 virtual 模式或自定义渲染器进行可见性判断 | 需结合 AreaPlugin 获取视口范围,显著提升大型画布的渲染性能。 |
| 简化节点 UI | 减少每个节点的 DOM 元素和复杂样式 | 避免在节点内使用过多嵌套、阴影、动画;使用轻量级组件 | 复杂 UI 是性能瓶颈主因,尤其在移动端。 |
| 防抖连接/拖拽 | 对频繁触发的事件(如鼠标移动)进行防抖处理 | lodash.debounce 包装 mousemove 事件:const debouncedMove = debounce(handleMove, 16); | 降低事件处理频率,避免卡顿。 |
| 减少 re-render | 避免不必要的组件重渲染(Vue/React) | Vue: 使用 v-memo 或 shouldComponentUpdate;React: React.memo | 确保节点状态变化时才更新 UI。 |
| 使用 Canvas 渲染 | 用 Konva 或原生 Canvas 替代 DOM 渲染 | 使用 rete-konva-render-plugin | Canvas 性能远超 DOM,适合超大型流程图(>1000 节点)。 |
| 懒加载连接线 | 连接线在节点被选中或悬停时才高亮/渲染 | 初始渲染为细线或隐藏,通过事件动态更新样式 | 减少初始绘制负担。 |
| 优化数据流 | 避免全图执行,仅执行受影响的子图 | 实现增量执行引擎或手动控制 engine.execute(node) 从特定节点开始 | 减少不必要的计算,提升响应速度。 |
| Web Worker 执行 | 将复杂计算或引擎执行放入 Web Worker,避免阻塞主线程 | 将 engine.process() 移至 Worker,通过 postMessage 通信 | 适用于 CPU 密集型逻辑,提升 UI 流畅度。 |
9.2 内存泄漏预防
| 泄漏来源 | 预防与解决方法 | 代码示例 | 注意事项 |
|---|---|---|---|
| 未清理事件监听 | 所有通过 on() 添加的监听器,必须在销毁时用 off() 移除 | const handler = () => {}; editor.on('nodecreated', handler);// 销毁时editor.off('nodecreated', handler); | 特别是自定义插件和 UI 组件中的监听。 |
| 插件未返回销毁函数 | 自定义插件的 install 方法应返回清理函数 | install(editor) {const interval = setInterval(...);return () => clearInterval(interval);} | 编辑器销毁时会自动调用返回的函数。 |
| DOM 引用残留 | 手动创建的 DOM 元素未从文档移除 | const el = document.createElement('div');container.appendChild(el);// 销毁时container.removeChild(el); | 确保所有动态元素都有明确的挂载和卸载逻辑。 |
| 闭包引用 | 事件处理器或定时器中引用了大对象或组件实例 | 避免在 worker 或事件中直接引用 editor 或 node 的深层属性 | 使用弱引用或仅传递必要数据。 |
| 缓存未清理 | 节点或引擎的 cache 数据累积 | 定期清理 node.cache 或执行后重置 | 特别是异步节点,避免缓存过期数据。 |
| 循环引用 | 对象 A 引用 B,B 又引用 A,导致 GC 无法回收 | 检查数据结构,避免节点间直接强引用 | 使用 WeakMap 或事件机制替代直接引用。 |
| 框架组件未卸载 | Vue/React 组件销毁时未清理 Rete 相关资源 | 在 beforeUnmount / useEffect cleanup 中移除事件、销毁编辑器 | onBeforeUnmount(() => { editor.destroy(); }); |
| 监控工具 | 使用浏览器开发者工具检测内存泄漏 | Chrome DevTools: Memory -> Heap Snapshot / Record Allocation Timeline | 定期检查是否存在对象持续增长。 |
9.3 跨框架兼容性问题
| 问题类型 | 描述与解决方案 | 代码示例或说明 | 注意事项 |
|---|---|---|---|
| 渲染器不匹配 | VueRenderPlugin 无法在 React 项目中使用 | 必须使用对应框架的渲染插件: - Vue 项目: rete-vue-render-plugin- React 项目: rete-react-render-plugin | 混用会导致渲染失败或空白。 |
| 版本冲突 | Rete.js 或插件版本与框架版本不兼容(如 Vue 2 vs Vue 3) | 仔细查看插件文档的 peerDependencies 例如: rete-vue-render-plugin@^2.0.0 支持 Vue 3 | 使用 npm ls 检查依赖树,避免多版本共存。 |
| TypeScript 类型问题 | 不同框架的类型定义(.d.ts)冲突或缺失 | 确保安装了正确的类型包:@types/rete 或插件自带类型必要时手动扩展类型 | 在 shims-vue.d.ts 或 global.d.ts 中声明模块。 |
| 状态同步问题 | 框架的响应式系统与 Rete 节点状态不同步 | Vue 3: 使用 ref / reactive 包装节点数据,并在变更时触发更新React: 使用 useState + useEffect | 避免直接修改非响应式对象。 |
| 事件机制差异 | 框架的事件系统(如 Vue 的 $emit)与 Rete 的 emit 混淆 | 明确区分: - Rete 事件: editor.emit('nodeselected', node)- 组件事件: this.$emit('update') | 建议封装一层事件总线避免混淆。 |
| 构建工具配置 | Vite/Webpack 对 ESM/CJS 模块处理不一致,导致导入失败 | 在 vite.config.js 或 webpack.config.js 中正确配置 resolve.alias 和 externals | 特别是使用 rete 和插件时,确保路径正确。 |
| SSR (服务端渲染) 不支持 | Rete.js 依赖 DOM 和浏览器 API,无法在 Node.js 环境运行 | 在 SSR 项目中使用 v-if / useEffect 延迟初始化,仅在客户端挂载 | <div v-if="isClient"><ReteEditor /></div> |
| 移动端适配 | 触摸事件与鼠标事件处理逻辑不同 | 确保 AreaPlugin 和 ConnectionPlugin 支持触摸(通常默认支持) | 测试拖拽、缩放等手势在移动设备上的表现。 |