一、背景
对于 Schema 驱动的低代码平台,style 是协议的必要组成部分。如果平台用 Schema 描述页面结构、属性、数据和行为,却不能对样式进行协议化声明与运行时解析,那么它的页面描述能力就是不完整的。
低代码的本质决定了样式必须走完整的协议链路:Schema 静态声明 → Runtime 解释执行 → Renderer 呈现。同时,Designer 必须能够产出这份样式声明------无论入口来自 AI 生成、源码编辑,还是属性面板。
Style Resolver 的引入不是孤立的"样式优化",而是平台能力模型的必要补全。
二、技术方案设计
2.1 Schema 中的 style 与四层链路
style 定义在 Schema 顶层,表示页面的视觉样式声明。
围绕 schema.style,样式链路分成四层:
text
Designer
负责产生和编辑样式声明
↓
Schema
负责持久化 style 定义
↓
Runtime
负责解析 style,得到可运行的样式结果
↓
Renderer
负责把解析结果呈现到真实 DOM
2.2 Runtime 的 Style Resolver 能力
schema.style 只是声明,不是可渲染结果。Runtime 需要具备 Style Resolver 能力,把声明转换成可消费的样式对象。
这层能力解决三件事:
- 解析样式值(静态值和表达式)
- 过滤不支持的样式字段,清理无效结果(undefined等数据)
2.3 受控的协议边界
低代码平台不能把任意 CSS 属性无约束地开放到 Schema 中,否则协议失去边界,各层也无法形成统一认知。
样式需要是受控的:平台声明支持哪些字段,Runtime 只解析这些字段,Designer 只暴露这些字段的编辑入口。
这样设计的价值:
- 协议边界清晰,避免无效或不安全字段进入渲染链路
- 扩展有统一基线,样式能力可持续演进
这也带来一个约束:新增样式属性需要先纳入协议边界。对于普通样式字段,扩展的是协议元信息与字段声明;对涉及特殊语义或转换规则的字段,还需要扩展 Runtime 逻辑。
三、具体实现
3.1 协议层:新增 style 字段
在 Schema 接口中添加:
ts
export interface Schema {
id: string
componentName: string
props?: Record<string, any>
style?: StyleSchema // 新增,StyleSchema 是一个受控的协议,只允许平台声明的样式字段
// ...
}
3.2 Runtime 层:实现 resolveStyle
目标:把 schema.style 从"样式声明"转换成"Renderer 可直接消费的样式结果"。
表达式解析带来的效果,是让样式不再只是固定值,而是可以跟着页面数据和状态动态变化。例如:
color: "{{state.theme === 'dark' ? '#fff' : '#111'}}",可以根据主题切换文字颜色display: "{{state.visible ? 'block' : 'none'}}",可以根据状态控制显示与隐藏
它背后依赖的是 Runtime 内部的表达式求值能力,整体原理可参考 低代码 Expression Engine:一个微型表达式解释器的设计。
resolveStyle 在这一层主要做三件事:
- 遍历
schema.style,识别每一个样式字段 - 对每个值做解析,静态值直接使用,表达式值交给表达式引擎求值
- 清理无效结果(undefined / null / 空字符),产出稳定的
resolvedStyle
对于白名单之外的字段,基本思路是按原字段名直接透传到 resolvedStyle,让自定义样式、动画、定位等能力也能落到页面上。
解析结果挂到节点解析结果上,随链路继续向下传递给 Renderer。
3.3 Renderer 层:传递 style prop
tsx
<Component {...item.resolvedProps} style={item.resolvedStyle} />
3.4 Designer 层:样式的产出入口
schema.style 的产出入口有三个:
属性面板(StyleSetter) :围绕 schema.style 做"读取字段 → 渲染编辑器 → 响应修改 → 回写 Schema"的闭环。以 fontSize 为例:
text
schema.style.fontSize
↓ 读取
PropertyPanel / StyleSetter 识别字段,渲染 NumberSetter
↓ 用户输入
onChange(newValue) → 触发 Command → 回写 schema.style.fontSize
源码编辑 :用户直接编辑 Schema JSON,手动维护 style 字段。
AI 生成 :System Prompt 中明确约束 AI 把视觉样式输出到 style 字段,而非 props,AI 生成的 Schema 天然携带 schema.style。
四、踩过的坑
4.1 接口边界不清:resolveObject 不该对外暴露
最初将 resolveStyle 和 resolveObject 同时作为公共接口,调用方不知道该用哪个,技术方案文档里也出现了"先调 resolveObject 再调 resolveStyle"的混乱描述。
问题根因在于没有区分"公共接口"和"内部工具"的职责:
- 公共接口 (
resolveStyle/resolveProps/resolveEvents):面向具体场景,语义明确,调用方直接使用 - 内部工具 (
resolveObject):通用的对象遍历与解析逻辑,只供上述接口内部复用,不对外暴露
明确这个边界后,调用方只看到语义清晰的公共接口,内部实现细节不再泄漏到外部。
4.2 props 残留样式的覆盖问题
引入 schema.style 后,StyleSetter 的修改却不生效------props: { fontSize: 16 } 会直接压过 style: { fontSize: 20 }。原因是物料组件还在从 props 解构样式字段并合并到 style,导致 props 里的值优先级更高。
面对这个问题,有三种处理思路:
- 方案一:运行时 merge ------解析时把
schema.style和 props 里的样式字段合并,schema.style优先。两套数据共存,无需迁移,但协议职责边界模糊。 - 方案二:schema.style 覆盖 ------Renderer 层让
resolvedStyle覆盖 props 里的同名字段。实现最简单,但 props 和 style 的边界还是不清晰,只是优先级问题被掩盖了。 - 方案三:全链路迁移 ------彻底清理 props 中的样式字段,
schema.style作为唯一来源,同步更新存量数据、AI Prompt 和物料组件。
项目没有历史数据包袱,我选了 方案三。代价是要同步改三处,但换来的是协议彻底干净:props 只承载业务属性,style 只承载样式属性,两者职责清晰,"干净利落"的协议为以后调试、问题排查和能力演进提供了基础。
五、成果与展望(todo)
5.1 成果
Runtime 层的 Style Resolver V1 已经形成完整闭环:Designer 负责产出和修改 schema.style,Runtime 负责解析,Renderer 负责最终呈现。
这意味着样式能力正式成为低代码平台的一条标准链路,属性设置到页面展示之间有了统一、稳定的实现方式。
最终效果:

5.2 展望
后续的演进方向:让 schema.style 不只是能表达基础样式,还能逐步承载主题、设计变量、自定义样式、响应式,甚至弹窗、脱离文档流、动画等更复杂的视觉效果。
我希望最终达到的效果是:基于这套能力,低代码平台不只可以搭普通页面,还可以做出布局复杂、视觉丰富、样式炫彩、交互更强的应用页面。