低代码 Runtime 全景:从 Schema 到可交互页面

一、Runtime 的角色与位置

低代码引擎的核心分层:

复制代码
Schema → 描述"要做什么"(结构 / 数据 / 行为)
Runtime → 决定"如何执行"(解析 / 计算 / 调度)
Renderer → 负责"如何呈现"(组件渲染)

Runtime 是中间的执行层------上接 Schema 的静态描述,下接 Renderer 的 UI 渲染。它的职责:

将声明式 Schema 转换为可执行模型,并统一驱动数据、行为与视图更新

Runtime 如何连接 Renderer:IRuntime 接口

复制代码
Schema ──→ Runtime(解析 + 求值 + 展开)──→ Renderer(模式匹配 + 渲染)

Runtime 和 Renderer 之间的唯一通道是 IRuntime 接口。Renderer 不直接读 Schema,不解析表达式,不处理 condition/loop------它只调 IRuntime 的方法,消费返回结果。

IRuntime 中同一个接口,对 Runtime 来说是"对外 API",对 Renderer 来说是"数据来源"。 这样的设计:同一套 Renderer,注入不同的 Runtime 实例,行为则完全不同

  • 设计态注入 DesignerRuntime:发请求、触发事件等都空实现------画布只关心设计体验(拖拽、选中等),不需要真正的交互逻辑
  • 运行态注入 PreviewRuntime:真正干活的 Runtime,实现所有能力

二、IRuntime 接口:7 个方法

IRuntime 是整个 Runtime 的"API 目录", 对外提供这些能力:

typescript 复制代码
interface IRuntime {
  // Schema Runtime ------ condition / loop / props / ctx 一次性解析
  resolveSchemaNode(schema, parentCtx): ResolvedSchemaNode
  // Expression Engine ------ 解析 props 中的 {{}} 表达式
  resolveProps(schema, scopeCtx): Record<string, any>
  // RuntimeContext ------ 创建当前节点的运行时上下文
  createContext(schema, resolvedProps, parentCtx): RuntimeContext
  // 调度系统 ------ 返回根 ctx,供 RendererRoot 订阅更新
  getRootContext(): RuntimeContext | undefined
  // Lifecycle ------ 执行 onMount / onUnmount
  runLifecycle(name, ctx, schema): void
  // DataSource ------ 执行 isInit 数据源请求
  runDataSource(ctx, schema): void
  // Events ------ 将事件声明包装成回调函数
  resolveEvents(schema): Record<string, Function>
}

其中 resolveSchemaNode 是 Bridge 的核心方法------一次性完成 condition 判断、loop 展开、props 解析、ctx 创建,Renderer 只需对返回的 kind 做模式匹配:

typescript 复制代码
// Renderer 侧的全部"逻辑"仅此而已
const result = runtime.resolveSchemaNode(schema, parentCtx)

if (result.kind === 'empty')  return null
if (result.kind === 'list')   return result.items.map(item => <RendererNodeItem />)
return <RendererNodeItem item={result} />

三、Runtime 核心组成

Runtime 拆解下来是 4 类能力:

类别 模块 解决什么问题
数据驱动 RuntimeContext + Expression Engine 数据存储 → 表达式计算 → UI 更新
结构执行 Schema Runtime(resolveSchemaNode) condition / loop / scope 控制流解释
副作用系统 Lifecycle + DataSource + Events 请求 / 生命周期 / 事件 → 改变 state
调度系统 ctx.set → notifyUpward → rerender 状态变更触发 UI 更新

将它们串起来就是 Runtime 的核心循环:

arduino 复制代码
Schema
  ↓ 结构执行 ── resolveSchemaNode(condition / loop / scope)
  ↓ 数据驱动 ── Expression Engine({{}} 求值)+ RuntimeContext(作用域)
  ↓
渲染(Renderer)
  ↓
副作用系统 ── Lifecycle / DataSource / Events → 改变 state
  ↓
调度系统 ── ctx.set → notifyUpward → rerender → 回到顶部重新解析

3.1 数据驱动:RuntimeContext + Expression Engine

RuntimeContext 是每个节点的运行时作用域,提供 5 个数据槽(state / props /等)和一条作用域链。子节点通过 parent 向上查找变量,和 JavaScript 作用域链机制一致。

Expression Engine 是一个手写的 Tokenizer + 递归下降 Parser + AST Evaluator,负责将 {{state.count + 1}} 求出真实值。详见 低代码 Runtime 策略注入:让表达式引擎真正可扩展

3.2 结构执行:Schema Runtime

resolveSchemaNode 一次性处理 Schema 的控制流语义------condition、loop、作用域传递,返回 ResolvedSchemaNode 联合类型,Renderer 只做模式匹配进行渲染。详见 低代码的逻辑解释能力:Schema Runtime 的设计

3.3 副作用系统:Lifecycle + DataSource + Events

三者共同解决"UI 怎么动起来":Lifecycle 控制执行时机,DataSource 提供声明式数据请求,Events 声明式用户交互能力。

3.4 调度系统:ctx.set → rerender

所有副作用最终通过 ctx.set() 改变状态。向上冒泡 + 整树 rerender。详见 低代码 Runtime 的调度系统:"数据变化 → UI更新"的最小架构


四、完整执行链路

以下面这段 Schema 为例,串联 Runtime 内部各模块如何协作:

json 复制代码
{
  "id": "page",
  "state": { "postList": [], "loading": false },
  "methods": { "loadList": "async function(ctx) { ... fetch + ctx.set ... }" },
  "lifeCycles": { "onMount": "function(ctx) { ctx.methods.loadList(ctx) }" },
  "dataSource": { "list": [{ "id": "authorList", "isInit": true, "..." }] },
  "children": [
    { "id": "refresh-btn", "events": { "onClick": "loadList" } },
    { "id": "loading-text", "condition": "state.loading" },
    { "id": "post-item", "loop": "state.postList", "props": { "children": "{{post.title}}" } }
  ]
}

主线流程可以简化为 4 步:

1. 首次渲染

text 复制代码
Renderer 启动
→ Runtime 解释根 schema
→ 创建根上下文 rootCtx
→ 解析 condition / loop / props
→ 渲染首屏内容

这一阶段的关键点是:首屏只渲染"当前条件下可见"的节点。

如果 condition 不满足,对应节点不会显示;如果 loop 对应的数据还是空数组,对应列表也不会展开。

2. 页面初始化逻辑执行

text 复制代码
节点挂载完成
→ Runtime 执行 onMount / DataSource
→ 开始加载数据

这里既可以执行命令式逻辑,比如 loadList();也可以触发声明式数据源请求,比如 fetch(...)

3. 数据变化,触发重新解释

text 复制代码
ctx.set(...)
→ notifyUpward
→ Renderer 重新渲染
→ Runtime 重新解释 schema

一旦数据变化:

  • condition 会重新判断
  • loop 会重新展开
  • 表达式会重新求值
  • UI 会自动更新

例如:

  • state.loading = true,加载中显示
  • state.postList = [...],文章列表展开
  • data.authorList = [...],作者列表展开

4. 用户交互再次触发同样流程

text 复制代码
用户点击按钮
→ 调用 methods / 更新 state
→ notifyUpward
→ Runtime 重新解释 schema
→ UI 更新

也就是说,用户交互和初始化加载,本质上走的是同一条链路:改数据 → 触发更新 → Runtime 重新解释 → Renderer 更新界面

这一段 JSON 代码,体现的就是 Runtime 在协调 7 个子系统的协作:Expression 解析数据绑定、Schema Runtime 处理 condition/loop、Lifecycle 触发初始化、DataSource 请求数据、Events 响应用户交互、RuntimeContext 提供作用域、调度系统驱动 UI 更新。


五、设计边界与演进方向

V1 的关键取舍

V1 优先保证最小闭环可用,做了几个有意的取舍:

V2 演进方向

方向 目标
调度系统 依赖追踪、局部更新、scheduler 批处理
事件系统 loop 内事件上下文、与 Runtime 集成
Expression Engine 三元表达式、函数调用(CallExpression)、AST 缓存
DataSource 传参、插件扩展、与 Runtime 深度结合
Lifecycle onUpdate等能力增强、async、表达式能力融入
Schema Runtime 能力增强 slot 等能力

六、总结

Runtime 能力一张表概括:

问题 核心模块 实现原理
数据从哪来? RuntimeContext 作用域链 + 5 个数据槽,子节点沿 parent 向上查找
表达式怎么算? Expression Engine 手写 Tokenizer + 递归下降 Parser + AST Evaluator
condition/loop 怎么处理? Schema Runtime resolveSchemaNode 一次性解释,返回 empty / single / list
组件什么时候执行逻辑? Lifecycle JSFunction 编译 + 缓存,useEffect 时机触发
数据请求怎么发? DataSource isInit 声明式请求,handler 注入,结果存入 ctx.data
用户交互怎么响应? Events 方法名引用 → 回调函数包装,合并到 resolvedProps
状态变了 UI 怎么更新? 调度系统 ctx.set → notifyUpward → rootCtx.subscribe → rerender
Renderer 怎么消费? Renderer Bridge IRuntime 7 个方法,Renderer 只做模式匹配

这些模块各司其职,通过 IRuntime 接口串联成完整的执行系统,Renderer 只需消费 IRuntime 返回的结果并完成渲染。最终效果:一段 JSON 驱动一个可交互的页面。

相关推荐
米丘6 小时前
微前端 Micro-App 实践
微服务·前端框架·前端工程化
one_love_zfl7 小时前
Conllect-LLM:一个低代码 AI Agent 构建平台的设计与实现
人工智能·低代码
weelinking16 小时前
【产品】12_接入数据库——让数据永久保存
jvm·数据库·python·react.js·数据挖掘·前端框架·产品经理
weixin_397574091 天前
AgentRAG与ReAct推理链:从检索增强到推理增强
前端·react.js·前端框架
多租户观察室1 天前
IDC 2026 中国低代码报告:市场规模突破 131 亿
低代码
Jeking2171 天前
低代码平台 表单设计器 unione form editor 功能组件 —— 按钮组件
低代码·动态表单·表单设计·表单引擎·unione cloud
_xaboy1 天前
开源Vue组件FormCreate通过 JSON 生成TinyVue表单
前端·vue.js·低代码·开源·json·表单设计器
WebInfra1 天前
TanStack Start 框架正式支持 Rsbuild
前端·javascript·前端框架