概念

TIP

建议先完成输出变量消费变量的动手实践,再回到本文作为参考手册。我们通过 🌟 标记出可优先掌握的概念。

阅读路径

  • 可以先快速浏览下方术语导航,确认自己要查的名词是否在其中。
  • 阅读「核心概念」关系图,建立变量、作用域、AST 的整体框架。
  • 按需跳转到对应小节,结合当前遇到的问题查阅细节,不必顺序阅读。
📖 术语快速查询
  • 概念本体
    • 变量 🌟:流程设计阶段定义出来、运行时才求值的数据容器。
    • 作用域 🌟:变量的容器,同时维护与其他作用域的依赖关系。
    • AST 🌟:作用域内变量信息的结构化存储方式。
  • AST 相关
    • ASTNode:AST 树中的节点,表示一段变量信息。
    • ASTNodeJSON:ASTNode 的 JSON 序列化形式。
    • 声明 🌟:标识符 + 定义,变量引擎的最小信息单元。
    • 类型 🌟:用于约束变量值范围的定义。
    • 表达式:输入若干变量后计算得到新变量。
  • 作用域关系

核心概念

变量引擎核心概念可以通过下图串起来理解:

变量核心概念关系图
读图重点
  • 绿色节点代表「信息是什么」,如变量、类型、表达式。
  • 红色节点代表「信息怎么存」,即 AST 节点。
  • 紫色节点代表「信息放在哪」,即作用域。
  • 虚线节点及线条代表「信息怎么流动」,即作用域链。

为了降低抽象程度,可以先记住一个真实案例:

「批处理节点」读取「上游 HTTP 节点」的数组输出 → 遍历得到 item → 在子节点里继续使用 item

这个过程涉及的所有名词都在下文出现,阅读时可随时对照。

变量

变量是在设计态定义、在运行态求值的数据容器。进一步了解可参考 变量介绍

⚠️ 变量在设计和运行中的关注点不同

在流程设计中,变量只关注定义,不关注值。变量的值在流程的运行时才会被动态计算。

作用域 🌟

作用域(Scope)是一种容器:容器内聚合了一系列变量信息,同时维护了与其他作用域的依赖关系。一句话概括:作用域决定「谁可以访问哪些变量」。

作用域的范围可以根据业务场景的不同约定,常见的三类如下:

场景示例
流程里节点可以约定为作用域节点作用域
全局变量侧边栏也可以约定为作用域全局作用域
界面编辑里组件(含变量)可以约定为作用域组件作用域
为什么 FlowGram 要在节点之外,新抽象一个作用域的概念?
  1. 节点 ≠ 作用域:同一个节点可能需要拆分成公开作用域与私有作用域。
  2. 存在与节点无关的作用域,如面向全局的变量抽屉。
  3. 部分节点需要多层作用域(例:循环的私有作用域),节点概念不足以描述。

AST 🌟

作用域通过 AST 存储变量信息。可以把它当作「变量信息」的树形结构:每个节点描述一个声明、类型或表达式。

TIP

通过 scope.ast 可以访问作用域内的 AST 树,从而对变量信息进行 CRUD 操作。

ASTNode

ASTNode 是变量引擎中用于存储变量信息基本信息单元。它可以为各种变量信息建模

  • 声明:如 VariableDeclaration ,用于声明新变量。
  • 类型:如 StringType,用于表示 String 类型。
  • 表达式:如 KeyPathExpression,用于对变量的引用。
ASTNode 具有以下特点
  • 树状结构: ASTNode 可以嵌套形成树(AST),表示复杂的变量结构。
  • 序列化: ASTNode 可以与 JSON 格式(ASTNodeJSON)相互转换,以便存储或传输。
  • 可扩展: 可以通过扩展 ASTNode 基类来添加新功能。
  • 响应式: ASTNode 值的变化会触发事件,从而实现响应式编程模式。

ASTNodeJSON

ASTNodeJSONASTNode纯 JSON 序列化表示。通常我们会在设计端构造它,再交由变量引擎实例化。

最关键的字段是 kind,用于表示 ASTNode 的类型:

/**
 * 相当于 JavaScript 代码:
 * `var var_index: string`
 */
{
  kind: 'VariableDeclaration',
  key: 'var_index',
  type: { kind: 'StringType' },
}

用户在使用变量引擎时,通过 ASTNodeJSON 描述变量信息,然后通过变量引擎实例化ASTNode,并将其添加到作用域中。

/**
 * 通过 scope.setVar 方法,将 ASTNodeJSON 实例化为 ASTNode,并添加到作用域中
 */
const variableDeclaration: VariableDeclaration = scope.setVar({
  kind: 'VariableDeclaration',
  key: 'var_index',
  type: { kind: 'StringType' },
});

/**
 * ASTNodeJSON 实例化为 ASTNode 之后,可以进行响应式监听
 */
variableDeclaration.onTypeChange((newType) => {
  console.log('变量类型变化了', newType);
})
概念比对

ASTNodeJSONASTNode 的关系,类似于 React 中 JSXVDOM 的关系

  • ASTNodeJSON 通过变量引擎实例化为 ASTNode
  • JSX 通过 React 引擎实例化为 VDOM
❓ 为什么不用 Json Schema

Json Schema 是一种用于描述 JSON 数据结构的格式:

  • Json Schema 只描述了变量的类型信息,而 ASTNodeJSON 还可以包含变量的其他信息(如:变量的初始值)。
  • ASTNodeJSON 可以通过变量引擎实例化为 ASTNode,从而实现响应式监听等能力。
  • Json Schema 擅长描述 Json 的类型,而 ASTNodeJSON 可以通过自定义扩展定义行为更复杂的信息。

在技术选型上,变量引擎内核需要更强大的扩展与表达能力,因此需要用 ASTNodeJSON 来描述更丰富更复杂的变量信息,如:通过定义变量的初始值,实现变量类型的动态推导 + 自动联动。

不过 Json Schema 作为业界通用的 JSON 类型描述格式,在易用性、跨团队沟通以及生态(如 ajv、zod)上更有优势。因此我们在物料库中大量使用了 Json Schema,来降低大家的上手成本。

TIP

变量引擎提供了 ASTFactory,可以类型安全地创建 ASTNodeJSON:

import { ASTFactory } from '@flowgram/editor';

/**
 * 类型安全地创建 VariableDeclaration ASTNodeJSON
 *
 * 等价于:
 * {
 *   kind: 'VariableDeclaration',
 *   key: 'var_index',
 *   type: { kind: 'StringType' },
 * }
 */
ASTFactory.createVariableDeclaration({
  key: 'var_index',
  type: { kind: 'StringType' },
});

声明 🌟

声明 = 标识符(Key) + 定义(Definition)。在设计态中,声明是一种存储标识符与变量信息的 ASTNode,是变量系统的最小「可被引用」单元。

  • 标识符(Key):访问声明的索引。
  • 定义(Definition):声明定义的信息。如:变量的定义 = 类型 + 右值。
举例:JavaScript 中的声明

变量声明 = 标识符 + 变量定义(类型 + 初始值)

/**
 * 标识符:some_var
 * 变量定义:类型为 number,初始值为 10
 */
const some_var: number = 10;

函数声明 = 标识符 + 函数定义(函数入参出参 + 函数体实现)

/**
 * 标识符:add
 * 函数定义:入参为两个 number 类型的变量 a, b,出参为 number 类型的变量
 */
function add(a: number, b: number): number {
  return a + b;
}

结构体声明 = 标识符 + 结构体定义(字段 + 类型)

/**
 * 标识符:Point
 * 结构体定义:字段为 x, y,类型均为 number
 */
interface Point {
  x: number;
  y: number;
}
标识符的作用
  • 标识符是声明的索引,用于访问声明中的定义
  • 举例:编程语言在编译时,通过标识符找到变量的类型定义,从而可以进行类型检查。

变量引擎目前只提供了变量字段声明BaseVariableField),并基于此扩展了变量声明VariableDeclaration)和属性声明Property)两种声明。

  • 变量字段声明(BaseVariableField)= 标识符 + 变量字段定义(类型 + 元信息 + 初始值)
  • 变量声明(VariableDeclaration)= 全局唯一标识符 + 变量定义(类型 + 元信息 + 初始值 + 作用域内排序)
  • 属性声明(Property)= Object 内唯一标识符 + 属性定义(类型 + 元信息 + 初始值)

类型 🌟

类型用于约束变量值的范围。在设计态中,类型也是一种 ASTNode。理解类型有助于掌握「变量能装什么」以及「表达式返回什么」。

变量引擎内置了 JSON 的基础类型

  • StringType:字符串
  • IntegerType:整数
  • NumberType:浮点数
  • BooleanType:布尔值
  • ObjectType:对象,可下钻 Property 声明。
  • ArrayType:数组,可下钻其他类型。

同时新增了:

  • MapType:键值对,键和值都可以进行类型定义。
  • CustomType:由用户进行自定义扩展,如日期、时间、文件类型等。

表达式

表达式输入 0 个或者多个变量,并通过特定方式进行计算,返回一个新的变量。设计态只描述「依赖了谁」和「推导出的类型」,运行态负责真正的值计算。

而在设计态中,表达式是一种 ASTNode,建模中我们只需关注:

  • 表达式使用了哪些变量声明 ?
  • 表达式的返回类型是怎么推导的 ?
举例:设计态中表达式的推导

假设我们有一个用 JavaScript 代码描述的表达式 ref_var + 1

表达式使用了哪些变量声明 ?

  • ref_var 标识符对应的变量声明

表达式的返回类型是怎么推导的 ?

  • ref_var 的类型为 IntegerType,则 ref_var + 1 的返回类型为 IntegerType
  • ref_var 的类型为 NumberType,则 ref_var + 1 的返回类型为 NumberType
  • ref_var 的类型为 StringType,则 ref_var + 1 的返回类型为 StringType
举例:变量引擎如何实现类型推导 + 联动
类型自动推导

图中展示了一个常见的例子:批处理节点引用前序节点的输出变量,对其进行遍历处理,得到一个 item 变量。其中 item 的变量类型会随着前序节点输出变量的类型而自动变化。

这个例子的 ASTNodeJSON 可表示为:

ASTFactory.createVariableDeclaration({
  key: 'item',
  initializer: ASTFactory.createEnumerateExpression({
    enumerateFor: ASTFactory.createKeyPathExpression({
      keyPath: ['start_0', 'arr']
    })
  })
})

变量的推导链路如下:

作用域链

作用域链(Scope Chain)定义了一个作用域可以引用哪些作用域的变量。可以把它理解成「可读变量的白名单」。变量引擎提供了抽象类,具体业务可以根据实际编排形式实现自定义的作用域链。

变量引擎内置了自由布局作用域链固定布局作用域链两种作用域链实现。

依赖作用域

依赖作用域 = 当前作用域可以访问哪些作用域的输出变量。

可以通过 scope.depScopes 访问作用域的依赖作用域

覆盖作用域

覆盖作用域 = 哪些作用域可以访问当前作用域的输出变量。

可以通过 scope.coverScopes 访问作用域的覆盖作用域

画布中的变量

FlowGram 在画布中定义了以下几种特殊的作用域:

节点作用域 🌟

又称节点公开作用域,作用域可以访问上游节点节点作用域的变量,同时其输出变量声明也可以被下游节点节点作用域访问。

节点作用域 可以通过 node.scope 来设置和获取,它的作用域链关系如下图所示:

WARNING

在默认的作用域逻辑中,子节点的 节点作用域 输出变量不可被父节点的下游节点 访问。

节点私有作用域

节点私有作用域的输出变量只能在当前节点节点作用域及其子节点节点作用域中访问。类似编程语言中私有变量的概念。

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

全局作用域

全局作用域的变量能被所有节点作用域和节点私有作用域访问,但是自身不依赖其他作用域。适用于配置、常量、环境变量等公共信息。

全局作用域的设置方式详见输出全局变量,他的作用域链关系如下图所示:

整体架构

架构图

变量引擎设计上遵循 DIP(依赖反转)原则,按照代码稳定性、抽象层次以及与业务的距离分为三层:

变量抽象层

抽象层是最稳定的一层,定义了 ASTNodeScopeScopeChain 等核心接口,为上层实现提供扩展约束。

变量实现层

这一层包含更贴近业务的实现,易随产品演化调整。引擎内置了一批稳定的 ASTNode 节点和 ScopeChain 实现;当业务需要时,可以通过依赖注入注册新的节点或覆盖已有实现。

变量物料层

最外层通过外观模式(Facade)提升易用性,将复杂能力封装成「物料」给使用者直接复用。

  • 变量物料的使用详见:物料