低代码的逻辑解释能力:Schema Runtime 的设计

一、背景

在现有的低代码 Runtime 中,我们已经具备了以下基础能力:

  • RuntimeContext:数据作用域管理
  • Expression Engine:表达式执行
  • Renderer 接入 Runtime:渲染层与运行时解耦
  • 基础响应式:ctx.set → UI 更新

看起来,我已经能做到"Schema → 渲染出 UI"。但实际上,此时的 Schema 只能描述静态结构,还无法表达动态逻辑。比如:

  • 如何控制某个组件是否渲染?(condition)
  • 如何根据数据渲染一个列表?(loop)
  • 循环渲染时,每一轮应该用什么上下文?(item.name、index 从哪来?)

这些问题指向了一个缺口:如何将 Schema 静态描述 解释为 可执行行为

让 Schema 具备以下三种能力:

  • condition:条件渲染
  • loop:循环渲染
  • scope:作用域控制

二、问题本质:Schema ≠ 逻辑

先看一个典型 Schema:

json 复制代码
{
  "component": "List",
  "loop": "{{ list }}",
  "children": {
    "component": "Text",
    "props": {
      "text": "{{ item.name }}"
    }
  }
}

这个结构表达了一个清晰意图:

对 list 进行循环

每一项渲染一个 Text

文本内容是 item.name

但问题是,这些"语义"不会自动执行。

谁去解析 loop?

item 是谁?

每一轮渲染的上下文如何构建?

所以结论很直接:

Schema 只是 DSL(领域描述语言),不足以驱动行为。

我们需要为它配一个真正的"行为解释器"


三、核心思路:引入 Schema Runtime

为了解决上述问题,我在 Runtime 里增加了一层:Schema Runtime(逻辑执行层)。

它的职责:解释 Schema 的语义,并生成可执行结果,给Renderer消费

它们三层的关系:

  • Schema = DSL 静态描述
  • Schema Runtime = 逻辑解释器
  • Renderer = 结果渲染器

四、实现方案

4.1 整体执行模型

text 复制代码
Schema
   ↓
Schema Runtime(解释结构化语义)
   ↓
ResolvedSchemaNode(标准化结果)
   ↓
Renderer(渲染 UI)

其中,底层 Runtime(RuntimeContext、Expression Engine)作为基础设施,为 Schema Runtime 提供执行环境。


4.2 Schema Runtime 的核心职责

1. 纯逻辑解释层

输入:Schema

输出:标准化的可渲染结果(ResolvedSchemaNode)

它的核心职责是:

把 Schema 的结构语义(condition / loop / scope)

转换成 Renderer 可消费的结果

2. 实现 Renderer 解耦

通过引入 Schema Runtime,Renderer 被彻底解耦:

Renderer 不再关心:

  • condition
  • loop
  • 表达式执行
  • 作用域

Renderer 只需要关心一件事:根据标准化结果去渲染组件

3. 标准化输出:ResolvedSchemaNode

typeScript 复制代码
type ResolvedSchemaNode =
  | { kind: 'empty' }
  | { kind: 'single'; node }
  | { kind: 'list'; nodes: node[] }

如上是 Schema Runtime 的输出,它将所有复杂语义统一收敛为三种结构:

  • empty:不渲染
  • single:单节点
  • list:节点集合

本质是把"复杂控制流"降维为"简单数据结构":

复杂的 condition / loop / scope

被统一转换为 Renderer 可理解的最小模型


4.3 执行机制

Schema Runtime 的执行是一个递归解释过程,对每个 Schema 节点依次执行如下步骤:

  1. 判断 condition → 决定是否继续
  2. 判断 loop → 决定是否展开
  3. 构建当前节点的作用域(ctx)
  4. 解析 props(表达式求值)
  5. 生成标准化节点(empty / single / list)

一句话总结:对每一个 Schema 节点,依次完成"语义解释 → 作用域构建 → 数据解析"

1. 对 loop 的特殊处理

loop 的本质是:为每一次迭代创建独立作用域,并在该作用域下执行子节点的逻辑:

这使得每个节点实例拥有独立的数据上下文,

从而支持嵌套列表或更复杂的组合场景。

2. 作用域链

Schema Runtime 通过 RuntimeContext 构建作用域链:

text 复制代码
rootCtx (state / props / methods)
  ↓
loopCtx (引入 item、index 到 locals)
  ↓
nodeCtx (解析后的 props)

变量查找遵循"就近原则 + 向上冒泡",类似 JavaScript 作用域链。


4.4 协作模型:边解析边渲染

当前方案中,Schema Runtime 与 Renderer 采用 "按需解释" 的协作模式:

  • 不预构建完整运行时树
  • 在渲染过程中,遇到一个节点就调用 resolveSchemaNode 即时解析

这样做的好处是:

  • 避免一次性解析整棵树(性能更优)
  • 天然支持动态数据变化(数据驱动时按需重新解析局部节点)
  • 减少中间数据结构(降低整体复杂度)

五、核心方法:resolveSchemaNode

Schema Runtime 的核心入口是 resolveSchemaNode

resolveSchemaNode(schema,parentCtx) 完整的执行流程如下:

text 复制代码
schema + parentCtx
       │
       ▼
 ① evaluateCondition(schema.condition, parentCtx)
       │
       ├── false → return { kind: 'empty' }
       │
       ▼ true
 ② 有 loop 字段?
       │
       ├── 无 → 路径 C(single)
       │          resolveProps → createContext
       │          → return { kind: 'single', ... }
       │
       ▼ 有
 ③ evaluateLoop(schema.loop, parentCtx)
       │
       ├── 空数组 → return { kind: 'empty' }
       │
       ▼ 非空
 ④ 遍历数组,对每个 item 执行:
       createLoopContext → resolveProps → createContext
       → 收集结果
       → return { kind: 'list', items: [...] }

示例说明(loop 路径)

假设 Schema结构:

json 复制代码
{
  "componentName": "Text",
  "loop": "state.list", 
  "loopArgs": ["item", "index"],
  "props": {
    "children": "{{item.name}} - No.{{index + 1}}" 
  }
}

state.list = [{name:'Alice'}, {name:'Bob'}],每次迭代做三件事:

Step 1:createLoopContext --- 创建循环子作用域

typescript 复制代码
parentCtx.createChild({
  id: 'text1_loop_0',
  locals: { item: { name: 'Alice' }, index: 0 }
})

把当前迭代的 item 和 index 放入 locals,形成一个新的作用域。

Step 2:resolveProps --- 在新作用域下解析 props

arduino 复制代码
"{{item.name}} - No.{{index + 1}}"
→ 在 loopCtx 下求值
→ "Alice - No.1"

Step 3:createContext --- 为节点实例创建自己的 ctx

最终每个循环迭代产出一个 { key, schema, resolvedProps, ctx },作用域链为:

text 复制代码
rootCtx (state.list, state.count, ...)
   │
   ├── loopCtx₀ (locals: { item: {name:'Alice'}, index: 0 })
   │      └── nodeCtx₀ (props: { children: 'Alice - No.1' })
   │
   └── loopCtx₁ (locals: { item: {name:'Bob'}, index: 1 })
          └── nodeCtx₁ (props: { children: 'Bob - No.2' })

六、几个关键设计决策

6.1 Runtime 和 Renderer 分离

通过 Schema Runtime 将"逻辑解释"和"UI 渲染"彻底分开,带来的好处:

  • Renderer 可以保持"纯 View":它只做渲染,不理解业务语义
  • Runtime 可以独立测试:不需要启动 React Renderer 就能验证 Schema 解析逻辑
  • 支持多渲染框架:同一套 Runtime 可以驱动 React Renderer 或 Vue Renderer
  • 设计态/运行态统一:同一个 Renderer 组件树,通过注入不同 Runtime 实现完全不同的行为

6.2 边解析边渲染 vs 预构建运行时树

当前,我选择了"边解析边渲染"的方案,

核心原因:React 本身就是递归渲染组件树,没有必要推翻现有结构再维护另一棵运行时树。

"边解析边渲染" 的策略,实现简单,可以快速验证。

两种方案对比:

维度 边解析边渲染 buildRuntimeTree
核心思路 Renderer 递归时,每个节点调 resolveSchemaNode 即时解析 先递归整棵 Schema 构建完整 RuntimeNode 树,再交给 Renderer 渲染
数据流 Schema → 逐节点解析 → Renderer 消费 Schema → 完整树 → Renderer 消费
与 React 的契合度 天然契合,React 本身就是递归组件树 需要额外的树、diff/同步机制
局部更新 未来可精确到单节点粒度更新 需要自己实现树 diff 才能局部更新
复杂度 低,Renderer + Runtime 职责清晰 高,多一层"运行时树"抽象

补充: buildRuntimeTree的优势:

  • 可以形成一棵完整的"运行时中间结构" ------ 可观测性、可调试性强
  • 先构建完整树,再渲染 ------ Runtime 拥有全局信息,更适合做"全局优化和调度"
  • 有一棵"完整的语义树" ------ 更适合做依赖追踪、局部更新、编译优化

从架构演进角度来看,未来在需要依赖追踪、局部更新或性能优化时, 更适合逐步向 buildRuntimeTree 模型演进。


七、总结与展望

通过引入 Schema Runtime 这一逻辑解释层,我将低代码渲染引擎的复杂度拆解为两个清晰的部分:

解释层:负责所有语义解析(condition、loop、scope)

渲染层:只负责最简单的组件映射和渲染

这种分层设计让系统更加可测试、可扩展、可维护。

未来,我会基于这个稳定的地基,继续接入更多 Schema 层面的逻辑能力,例如:

  • slot(插槽)
  • fragment(片段)
  • portal(传送门)

让低代码平台真正拥有"声明式逻辑描述"的能力。

相关推荐
露临霜4 小时前
对低代码未来的思考
低代码
搭贝2 天前
建筑多分支企业数字化实战:凯驿景澄建设项目管理系统落地案例
大数据·人工智能·低代码·数字化·工程项目技术方案
Jeking2172 天前
低代码平台表单设计器 unione-form-editor 组件介绍 -- 数据字典
低代码·动态表单·表单设计·表单引擎·unione cloud
冲浪中台2 天前
【无标题】
前端·低代码
API开发平台2 天前
开源 API 开发平台 4.5.0 发布
低代码·开源
梦梦代码精2 天前
电商系统的核心难点:订单与营销系统如何设计?——LikeShop 架构深度拆解(规则计算与状态一致性)
java·开发语言·低代码·架构·开源·github
canonical_entropy3 天前
下一代低代码渲染框架 nop-chaos-flux 的设计原则
前端·低代码·前端框架
数智化管理手记3 天前
设备总停机?找准根源+TPM核心逻辑,筑牢零故障基础
数据库·人工智能·低代码·制造
Jeking2173 天前
低代码平台核心组件表单设计器 unione-form-editor 组件属性介绍--文本输入框
低代码·动态表单·表单设计·表单引擎·unione cloud