输出变量

我们主要将输出变量分为三类:

  1. 输出节点变量:通常作为该节点的产出,供后续节点使用。
  2. 输出节点私有变量:输出变量仅限于节点内部(包括子节点),不能被外部节点访问。
  3. 输出全局变量:贯穿整个流程,任何节点都可以读取,适合存放一些公共状态或配置。

输出节点变量

输出节点变量,意味着这个变量和当前节点的生命周期是绑定的。当节点被创建时,变量就诞生了;当节点被删除时,变量也随之消失。

我们通常有三种方式来输出节点变量:

方式一:通过表单副作用同步

表单副作用 通常在节点的 form-meta.ts 文件中进行配置,是定义节点输出变量最常见的方式。

provideJsonSchemaOutputs 物料

若节点所需输出变量的结构与 JSON Schema 结构匹配,即可使用 provideJsonSchemaOutputs 这一副作用(Effect)物料。

只需要在 formMetaeffect 中加上两行配置即可:

form-meta.ts
import {
  syncVariableTitle,
  provideJsonSchemaOutputs,
} from '@flowgram.ai/form-materials';

export const formMeta = {
  effect: {
    title: syncVariableTitle, // 变量标题自动同步
    outputs: provideJsonSchemaOutputs,
  },
};

通过 createEffectFromVariableProvider 自定义输出

provideJsonSchemaOutputs 只适配 JsonSchema。如果你想要定义自己的一套 Schema,那么就需要自定义表单的副作用。

NOTE

Flowgram 提供了 createEffectFromVariableProvider,只需要定义一个 parse函数,就可以自定义自己的变量同步副作用:

  • parse 会在表单值初始化和更新时被调用
  • parse 的输入为当前字段的表单的值
  • parse 的输出为变量 AST 信息

下面这个例子中,我们为表单的两个字段 path.to.valuepath.to.value2 分别创建了输出变量:

form-meta.ts
import {
  createEffectFromVariableProvider,
  ASTFactory,
  type ASTNodeJSON
} from '@flowgram.ai/fixed-layout-editor';

export function createTypeFromValue(value: string): ASTNodeJSON | undefined {
  switch (value) {
    case 'string':
      return ASTFactory.createString();
    case 'number':
      return ASTFactory.createNumber();
    case 'boolean':
      return ASTFactory.createBoolean();
    case 'integer':
      return ASTFactory.createInteger();

    default:
      return;
  }
}

export const formMeta =  {
  effect: {
    // Create first variable
    // = node.scope.setVar('path.to.value', ASTFactory.createVariableDeclaration(parse(v)))
    'path.to.value': createEffectFromVariableProvider({
      // parse form value to variable
      parse(v: string) {
        return {
          meta: {
            title: `Your Output Variable Title`,
          },
          key: `uid_${node.id}`,
          type: createTypeFromValue(v)
        }
      }
    }),
    // Create second variable
    // = node.scope.setVar('path.to.value2', ASTFactory.createVariableDeclaration(parse(v)))
    'path.to.value2': createEffectFromVariableProvider({
      // parse form value to variable
      parse(v: string) {
        return {
          meta: {
            title: `Your Output Variable Title 2`,
          },
          key: `uid_${node.id}_2`,
          type: createTypeFromValue(v)
        }
      }
    }),
  },
  render: () => (
    // ...
  )
}

多个表单字段同步到一个变量上

如果多个字段同步到一个变量,就需要用到 createEffectFromVariableProvidernamespace 字段,将多个字段的变量数据同步到同一个命名空间上。

form-meta.ts
import {
  createEffectFromVariableProvider,
  ASTFactory,
} from '@flowgram.ai/fixed-layout-editor';

/**
 * 从 form 拿到多个字段的信息
 */
const variableSyncEffect = createEffectFromVariableProvider({
  // 必须添加,确保不同字段的副作用,同步到同一个命名空间上
  namespace: 'your_namespace',

  // 将表单值解析为变量
  parse(_, { form, node }) {
    return {
      meta: {
        title: `Your Output Variable Title`,
      },
      key: `uid_${node.id}`,
      type: createTypeFromValue({
        value1: form.getValueIn('path.to.value'),
        value2: form.getValueIn('path.to.value2'),
      })
    }
  }
})

export const formMeta = {
  effect: {
    'path.to.value': variableSyncEffect,
    'path.to.value2': variableSyncEffect,
  },
  render: () => (
   // ...
  )
}

在副作用中使用 node.scope API

如果 createEffectFromVariableProvider 不能满足你的需求,你也可以直接在表单副作用中使用 node.scope API,进行更加灵活多变的变量操作。

NOTE

node.scope 会返回一个节点的变量作用域(Scope)对象,这个对象上挂载了几个核心方法:

  • setVar(variable): 设置一个变量。
  • setVar(namespace, variable): 在指定的命名空间下设置一个变量。
  • getVar(): 获取所有变量。
  • getVar(namespace): 获取指定命名空间下的变量。
  • clearVar(): 清空所有变量。
  • clearVar(namespace): 清空指定命名空间下的变量。
form-meta.tsx
import { Effect } from '@flowgram.ai/editor';

export const formMeta = {
  effect: {
    'path.to.value': [{
      event: DataEvent.onValueInitOrChange,
      effect: ((params) => {
        const { context, value } = params;

        context.node.scope.setVar(
          ASTFactory.createVariableDeclaration({
            meta: {
              title: `Your Output Variable Title`,
            },
            key: `uid_${node.id}`,
            type: createTypeFromValue(value),
          })
        )

        console.log("查看生成的变量", context.node.scope.getVar())

      }) as Effect,
    }],
    'path.to.value2': [{
      event: DataEvent.onValueInitOrChange,
      effect: ((params) => {
        const { context, value } = params;

        context.node.scope.setVar(
          'namespace_2',
          ASTFactory.createVariableDeclaration({
            meta: {
              title: `Your Output Variable Title 2`,
            },
            key: `uid_${node.id}_2`,
            type: createTypeFromValue(value),
          })
        )

        console.log("查看生成的变量", context.node.scope.getVar('namespace_2'))

      }) as Effect,
    }],
  },
  render: () => (
    // ...
  )
}

方式二:通过插件同步变量

除了在表单中静态配置,我们还可以在插件(Plugin)中,通过 node.scope 更新自由动态地操作节点的变量。

指定节点的 Scope 进行更新

下面的例子演示了如何在插件的 onInit生命周期中,获取开始节点的 Scope,并对它的变量进行一系列操作。

sync-variable-plugin.tsx
import {
  FlowDocument,
  definePluginCreator,
  PluginCreator,
} from '@flowgram.ai/fixed-layout-editor';

export const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions> =
  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({
    onInit(ctx, options) {
      const startNode = ctx.get(FlowDocument).getNode('start_0');
      const startScope =  startNode.scope!

      // Set Variable For Start Scope
      startScope.setVar(
        ASTFactory.createVariableDeclaration({
          meta: {
            title: `Your Output Variable Title`,
          },
          key: `uid`,
          type: ASTFactory.createString(),
        })
      )
    }
  })

在 onNodeCreate 同步变量

下面的例子演示了如何通过 onNodeCreate 获取到新创建节点的 Scope,并通过监听 node.form.onFormValuesChange 实现变量的同步操作。

sync-variable-plugin.tsx
import {
  FlowDocument,
  definePluginCreator,
  PluginCreator,
} from '@flowgram.ai/fixed-layout-editor';

export const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions> =
  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({
    onInit(ctx, options) {
      ctx.get(FlowDocument).onNodeCreate(({ node }) => {
        const syncVariable = (title: string) => {
          node.scope?.setVar(
            ASTFactory.createVariableDeclaration({
              key: `uid_${node.id}`,
              meta: {
                title,
                icon: iconVariable,
              },
              type: ASTFactory.createString(),
            })
          );
        };

        if (node.form) {
          // sync variable on init
          syncVariable(node.form.getValueIn('title'));

          // listen to form values change
          node.form?.onFormValuesChange(({ values, name }) => {
            // title field changed
            if (name.match(/^title/)) {
              syncVariable(values[name]);
            }
          });
        }
      });
    }
  })

方式三:在 UI 中同步变量(不推荐)

WARNING

直接在 UI 中同步变量(方式三)是一种非常不推荐的做法。

它会打破数据和渲染分离的原则,会导致数据和渲染之间的紧密耦合:

  • 脱离 UI 无法同步变量:脱离了 UI 就无法独立更新变量,会导致数据和渲染之间的不一致。
  • 增加代码复杂度:直接在 UI 中操作变量,会增加 UI 逻辑的复杂性,使代码更难维护。
  • 性能问题:变量的同步操作可能会触发 UI 组件的不必要的重渲染。

下面的例子演示了如何在 formMeta.render 中,通过 useCurrentScope 事件,同步更新变量。

form-meta.ts
import {
  createEffectFromVariableProvider,
  ASTFactory,
} from '@flowgram.ai/fixed-layout-editor';

/**
 * 从 form 拿到多个字段的信息
 */
const FormRender = () => {
  /**
   * 获取到当前作用域,用于后续设置变量
   */
  const scope = useCurrentScope()

  return <>
    <UserCustomForm
      onValuesChange={(values) => {
        scope.setVar(
          ASTFactory.createVariableDeclaration({
            meta: {
              title: values.title,
            },
            key: `uid`,
            type: ASTFactory.createString(),
          })
        )
      }}
    />
  </>
}

export const formMeta = {
  render: () => <FormRender />
}

输出节点私有变量

私有变量是指只能在当前节点及其子节点中访问的变量。

私有变量可以通过私有作用域 node.privateScope 来设置和获取,它的作用域链关系如下图所示:

下面只列举其中两种方式,其他方式可以根据输出节点变量的方式类推

方式一:通过 createEffectFromVariableProvider

createEffectFromVariableProvider 提供了参数 scope,用于指定变量的作用域。

  • scope 设置为 private 时,变量的作用域为当前节点的私有作用域 node.privateScope
  • scope 设置为 public 时,变量的作用域为当前节点的作用域 node.scope
form-meta.ts
import {
  createEffectFromVariableProvider,
  ASTFactory,
} from '@flowgram.ai/fixed-layout-editor';

export const formMeta =  {
  effect: {
    // Create variable in privateScope
    // = node.privateScope.setVar('path.to.value', ASTFactory.createVariableDeclaration(parse(v)))
    'path.to.value': createEffectFromVariableProvider({
      scope: 'private',
      // parse form value to variable
      parse(v: string) {
        return {
          meta: {
            title: `Your Private Variable Title`,
          },
          key: `uid_${node.id}_locals`,
          type: createTypeFromValue(v)
        }
      }
    }),
  },
  render: () => (
    // ...
  )
}

方式二:通过 node.privateScope

node.privateScope 的 API 设计得和节点作用域(node.scope)几乎一模一样,都提供了 setVargetVarclearVar等方法,并且同样支持命名空间(namespace)。详情可以参考 node.scope

form-meta.tsx
import { Effect } from '@flowgram.ai/editor';

export const formMeta = {
  effect: {
    'path.to.value': [{
      event: DataEvent.onValueInitOrChange,
      effect: ((params) => {
        const { context, value } = params;

        context.node.privateScope.setVar(
          ASTFactory.createVariableDeclaration({
            meta: {
              title: `Your Private Variable Title`,
            },
            key: `uid_${node.id}`,
            type: createTypeFromValue(value),
          })
        )

        console.log("查看生成的变量", context.node.privateScope.getVar())

      }) as Effect,
    }],
  },
  render: () => (
    // ...
  )
}

输出全局变量

全局变量就像是整个流程的“共享内存”,任何节点、任何插件都可以访问和修改它。它非常适合用来存储一些贯穿始终的状态,比如用户信息、环境配置等等。

和节点变量类似,我们也有两种主要的方式来获取全局变量的作用域(GlobalScope)。

方式一:在插件中获取

在插件的上下文中(ctx),我们可以直接“注入”GlobalScope 的实例:

global-variable-plugin.tsx
import {
  GlobalScope,
  definePluginCreator,
  PluginCreator
} from '@flowgram.ai/fixed-layout-editor';


export const createGlobalVariablePlugin: PluginCreator<SyncVariablePluginOptions> =
  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({
    onInit(ctx, options) {
      const globalScope = ctx.get(GlobalScope)

      globalScope.setVar(
         ASTFactory.createVariableDeclaration({
          meta: {
            title: `Your Output Variable Title`,
          },
          key: `your_variable_global_unique_key`,
          type: ASTFactory.createString(),
        })
      )
    }
  })

方式二:在 UI 中获取

如果你想在画布的 React 组件中与全局变量交互,可以使用 useService 这个 Hook 来获取 GlobalScope 的实例:

global-variable-component.tsx
import {
  GlobalScope,
  useService,
} from '@flowgram.ai/fixed-layout-editor';

function GlobalVariableComponent() {

  const globalScope = useService(GlobalScope)

  // ...

  const handleChange = (v: string) => {
    globalScope.setVar(
      ASTFactory.createVariableDeclaration({
        meta: {
          title: `Your Output Variable Title`,
        },
        key: `uid_${v}`,
        type: ASTFactory.createString(),
      })
    )
  }

  return <Input onChange={handleChange}/>
}

全局作用域的 API

GlobalScope 的 API 设计得和节点作用域(node.scope)几乎一模一样,都提供了 setVargetVarclearVar 等方法,并且同样支持命名空间(namespace)。详情可以参考 node.scope

下面是一个在插件中操作全局变量的综合示例:

sync-variable-plugin.tsx
import {
  GlobalScope,
} from '@flowgram.ai/fixed-layout-editor';

// ...

onInit(ctx, options) {
  const globalScope = ctx.get(GlobalScope);

  // 1. Create, Update, Read, Delete Variable in GlobalScope
  globalScope.setVar(
    ASTFactory.createVariableDeclaration({
      meta: {
        title: `Your Output Variable Title`,
      },
      key: `your_variable_global_unique_key`,
      type: ASTFactory.createString(),
    })
  )

  console.log(globalScope.getVar())

  globalScope.clearVar()


  // 2.  Create, Update, Read, Delete Variable in GlobalScope's namespace: 'namespace_1'
    globalScope.setVar(
      'namespace_1',
      ASTFactory.createVariableDeclaration({
        meta: {
          title: `Your Output Variable Title 2`,
        },
        key: `uid_2`,
        type: ASTFactory.createString(),
      })
  )

  console.log(globalScope.getVar('namespace_1'))

  globalScope.clearVar('namespace_1')

  // ...
}

详见:Class: GlobalScope