一、背景
在低代码平台中,Expression Engine(表达式引擎) 是 Runtime 的核心能力之一。
它负责将 Schema 中的动态表达式(如 {{user.name}})解析为运行时真实数据,从而驱动 UI 渲染。
二、概述
在我的低代码平台中,Expression Engine 的执行不是简单的函数调用, 而是由 Runtime 驱动的一套完整运行时执行流程。
🔁 最小闭环
graph TD
Runtime[PreviewRuntime] -->|选择evaluator| resolveProps
resolveProps -->|遍历props| resolveTemplate
resolveTemplate -->|解析模板| evaluateExpression
evaluateExpression -->|返回结果| resolveTemplate
resolveTemplate --> resolveProps
resolveProps --> Runtime
🎯 Runtime 的核心职责
PreviewRuntime 在这个闭环中承担的关键角色:
1️⃣ 调度中心
统一驱动整个执行流程
2️⃣ 策略控制点
决定本次执行使用哪一种 evaluator(表达式执行策略)
3️⃣ 运行时上下文构建
提供表达式执行所需的数据环境
🔑 关键设计点
我在这里做了一个核心解耦:
- resolveTemplate 不直接依赖 evaluateExpression
- 表达式执行策略由 Runtime 决定
- 并通过参数(evaluator)向下传递
三、核心原理
我的这套设计是将系统拆分为两类角色:
稳定流程层
负责执行通用流程,不关心具体策略:
- resolveProps(遍历)
- resolveTemplate(解析)
策略决策层
负责真正的策略选择,决定“这次到底怎么求值”:
- PreviewRuntime(选择 evaluator)
🧩 执行模型
所以流程是:
Runtime 决定策略
↓
resolveProps 传递策略
↓
resolveTemplate 消费策略
这就是 “上层决定行为,底层只执行流程”
为什么这比直接调用更好?
如果 resolveTemplate 直接 import evaluateExpression ,会出现一个问题:
- resolveTemplate 本来只是“模板壳处理工具”
- 结果它变成了“绑定某一套表达式引擎的模板处理器”
而我现在这种写法,把职责切分的很清楚:
- resolveTemplate 只负责识别“壳子”
- resolveProps 只负责遍历 props
- PreviewRuntime 才真正拥有“解析策略决定权”
也就是:把“表达式如何求值”从底层工具中抽离出来,交给 Runtime层 以策略注入的方式统一管理。
这就是典型的 策略注入
四、这个思路的优点
1. 职责更清晰
Runtime 决定“用什么策略”
工具函数只负责“执行流程”
2. 可替换性
以后完全可以替换 evaluator,而不用改“执行流程”
比如未来可能有:safeEvaluateExpression、designerEvaluateExpression等,都可以复用同一套流程
3. 易测试
比如测 resolveTemplate 时,无需跑完整表达式引擎,只要传一个 mock evaluator
这就把“流程测试”和“能力测试”拆开了
4. 扩展性
更利于扩展多运行态,比如 DesignerRuntime、PreviewRuntime 它们可以使用不同规则的 evaluator 解析:
- 设计态 Runtime 传递一个轻量 evaluator
- 运行态 Runtime 传递一个完整 evaluator
5. 开闭原则
对扩展开放,对修改关闭
新增一套 evaluator,不需要改 “执行流程”,只需要让 Runtime 改变注入策略即可
五、适用场景
我总结了可以在以下场景考虑这种方案:
1. 流程稳定,但策略可变
比如:日志上报流程固定(收集 → 格式化 → 上报),但策略不同:
- 开发环境:console.log
- 测试环境:mock上报
- 生产环境:真实埋点系统
2. 多个模式切换
比如:低代码类编辑器系统表达式执行:
- 设计态:需要高容错(轻量表达式解析)
- 运行态:严格执行 + 高性能(完整表达式解析)
3. 上层需要控制行为,而不是在工具函数里
比如:缓存策略,同一个 getData 流程:
- 有的场景需要强缓存
- 有的需要实时请求
六、总结
策略注入 这种模式本质是“稳定流程 + 可替换策略”的设计思维。
通过策略注入,可以把“变化点”从流程中抽离出来,
从而提升系统的扩展性、可测试性和长期演进能力。