一、背景
在现有的低代码 Runtime 中,我已经具备了以下基础能力:
- RuntimeContext:数据作用域管理
- Expression Engine:表达式执行
- Renderer 接入 Runtime:渲染层与运行时解耦
- 基础响应式:ctx.set → UI 更新
看起来,我已经能做到“Schema → 渲染出 UI”。但实际上,此时的 Schema 只能描述静态结构,还无法表达动态逻辑。比如:
- 如何控制某个组件是否渲染?(condition)
- 如何根据数据渲染一个列表?(loop)
- 循环渲染时,每一轮应该用什么上下文?(item.name、index 从哪来?)
这些问题指向了一个缺口:如何将 Schema 静态描述解释为可执行行为
让 Schema 具备以下三种能力:
- condition:条件渲染
- loop:循环渲染
- scope:作用域控制
二、问题本质:Schema ≠ 逻辑
先看一个典型 Schema:
{
"component": "List",
"loop": " "state.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 整体执行模型
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
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 节点依次执行如下步骤:
- 判断 condition → 决定是否继续
- 判断 loop → 决定是否展开
- 构建当前节点的作用域(ctx)
- 解析 props(表达式求值)
- 生成标准化节点(empty / single / list)
一句话总结:对每一个 Schema 节点,依次完成“控制语义解析 → 作用域构建 → 表达式解析”
1. 对 loop 的特殊处理
loop 的本质是:为每一次迭代创建独立作用域,并在该作用域下执行子节点的逻辑。
这使得每个节点实例拥有独立的数据上下文,从而支持嵌套列表或更复杂的组合场景。
2. 作用域链
Schema Runtime 通过 RuntimeContext 构建作用域链:
parentCtx (state / props / methods)
↓
loopCtx (引入 item、index 到 locals)
↓
childNodeCtx (解析后的 props)
变量查找遵循“就近原则 + 向上冒泡”,类似 JavaScript 作用域链。
4.4 协作模型:边解析边渲染
当前方案中,Schema Runtime 与 Renderer 采用 “按需解释” 的协作模式:
- 不预构建完整运行时树
- 在渲染过程中,遇到一个节点就调用 resolveSchemaNode 即时解析
这样做的好处是:
- 避免一次性解析整棵树(性能更优)
- 天然支持动态数据变化(数据驱动时按需重新解析局部节点)
- 减少中间数据结构(降低整体复杂度)
五、核心方法:resolveSchemaNode
Schema Runtime 的核心入口是 resolveSchemaNode
resolveSchemaNode(schema,parentCtx) 完整的执行流程如下:
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结构:
{
"componentName": "Text",
"loop": "state.list",
"loopArgs": ["item", "index"],
"props": {
"children": "{{item.name}} - No.{{index + 1}}"
}
}
以 state.list = [{name:'Alice'}, {name:'Bob'}],每次迭代做三件事:
Step 1:createLoopContext — 创建循环子作用域
parentCtx.createChild({
id: 'text1_loop_0',
locals: { item: { name: 'Alice' }, index: 0 }
})
把当前迭代的 item 和 index 放入 locals,形成一个新的作用域。
Step 2:resolveProps — 在新作用域下解析 props
"{{item.name}} - No.{{index + 1}}"
→ 在 loopCtx 下求值
→ "Alice - No.1"
Step 3:createContext — 为节点实例创建自己的 ctx
最终每个循环迭代产出一个 { key, schema, resolvedProps, ctx },作用域链为:
parentCtx (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 预构建运行时树(buildRuntimeTree)
在设计 Schema Runtime 时,我面临一个核心选择:
是采用“解释执行(边解析边渲染)”,
还是“构建中间运行时树(buildRuntimeTree)”。
我选择前者,是因为:
- 更贴合 React 渲染模型
- 降低实现复杂度
- 更适合当前阶段快速迭代
但我也清楚: 从架构演进角度来看,后者有一棵“完整的语义树”,更适合做依赖追踪、局部更新、编译优化 —— 能力更强。
七、总结与展望
通过引入 Schema Runtime 这一逻辑解释层,我将低代码渲染引擎的复杂度拆解为两个清晰的部分:
解释层:负责所有语义解析(condition、loop、scope)
渲染层:只负责最简单的组件映射和渲染
这种分层设计让系统更加可测试、可扩展、可维护。
未来,我会基于这个稳定的地基,继续接入更多 Schema 的逻辑能力,例如:
- slot(插槽)
- fragment(片段)
- portal(传送门)
让低代码平台“声明式逻辑描述”的能力更加强大。