低代码系统:Schema、渲染器与插件化架构
从页面 DSL 到编辑器、运行时渲染和发布,梳理低代码平台的核心边界与工程取舍。
低代码系统:Schema、渲染器与插件化架构
本文聚焦低代码系统的设计边界、演进方式和工程取舍。
1. 低代码平台解决什么问题
低代码平台把“组件、属性、布局、数据和交互”编码成可保存、可迁移的 Schema,再由编辑器和运行时共同解释它。它不是简单的拖拽页面:真正困难的是 Schema 的长期兼容、组件能力治理、表达式安全和编辑态/运行态的一致性。
建议拆成四层:
物料层:组件、属性定义、事件、图标、文档
编辑层:画布、选中/拖拽、属性面板、撤销重做
运行层:Schema 校验、数据绑定、布局与事件执行
发布层:版本、预览、权限、导出、回滚
编辑器负责产生 Schema,运行时只消费经过校验和迁移的 Schema;不要让画布组件直接依赖编辑器内部状态。
1.1 先做领域建模,再抽象 DSL
DSL 不是把页面字段随意堆成 JSON。设计前应先盘点领域中已经出现过的页面形态、交互和数据流,再把稳定的共性抽成模型。这样既能让搭建器复用模板,也能为后续比较同类页面、分析业务效果留下可用的数据维度。
以运营弹窗这类领域为例,可以先按职责拆出以下模块:
- 基础信息:名称、类型和版本等元数据。
- 组件:图片、文字、倒计时、热区、容器、视频、状态和关闭按钮等物料。
- 样式:跨 H5、iOS、Android 都能解释的最小样式集合,避免把某个终端私有属性写成通用协议。
- 请求:曝光前后的请求描述,并明确串行、并行和失败策略。
- 行为:用户事件与声明式动作,和组件的展示属性分离。
字段应保持精简、模块高内聚低耦合,并优先通过增加受控属性值支持新需求,而不是频繁改变结构。DSL 的每次变更都要有文档、兼容策略和迁移记录;废弃或只在部分终端生效的字段必须显式标注。
来源:大促搭投实践&高可用性保障(简化版).pdf 第 2-6 页。
2. Schema 设计要点
Schema 至少需要稳定的版本号、节点 ID、组件类型、受控属性和子节点:
{
"schemaVersion": "1.0",
"root": {
"id": "page-root",
"type": "Page",
"props": { "title": "订单列表" },
"children": [
{
"id": "orders-table",
"type": "DataTable",
"props": { "columns": ["id", "status"] },
"dataSource": { "kind": "query", "name": "orders" }
}
]
}
}
关键约束:
id在文档内稳定且唯一,便于选中、差异比较和埋点。type只引用物料注册表中的类型,不允许任意组件路径。- 属性值与事件动作分离,避免把函数序列化进 JSON。
- 数据源、权限和敏感配置使用引用或服务端 ID,不能把密钥写入页面 Schema。
- 用 JSON Schema/Valibot 等运行时校验器限制字段类型、深度、数组长度和字符串大小。
Schema 升级必须有迁移函数,例如 1.0 -> 1.1;读取旧版本时先迁移到当前内部模型,再交给渲染器。不要在各个组件里散落版本判断。
2.1 把行为和请求建模成协议
行为协议应描述“什么事件触发、执行什么动作、作用于谁以及携带什么内容”,而不是把函数直接序列化进 Schema。一个通用的动作可以表示为:
{
"type": "click",
"behavior": "switch",
"target": "success-state",
"content": null
}
type 可以是 click、tabSwitch 等事件;behavior 应限制在经过审核的动作集合,例如 navigate、switch、request、close、show 和 hide;target 和 content 的含义由动作类型决定。一个事件通常只执行一个动作,确有组合需求时才使用有序的 actions 列表,并逐项校验权限、参数和失败处理。
请求也应作为声明式数据描述,复用于曝光前和曝光后流程,并明确串行或并行关系、超时、重试和降级策略。运行时只执行白名单动作,不能使用 eval 或 new Function 解释任意用户输入。
来源:大促搭投实践&高可用性保障(简化版).pdf 第 3-4 页。
3. 物料注册表与渲染器
物料定义应同时描述运行时组件、可编辑属性、事件和默认值:
type ComponentMeta = {
type: string
version: string
render: React.ComponentType<Record<string, unknown>>
propsSchema: unknown
events?: string[]
defaults?: Record<string, unknown>
}
const registry = new Map<string, ComponentMeta>()
function register(meta: ComponentMeta) {
if (registry.has(meta.type)) throw new Error(`duplicate component: ${meta.type}`)
registry.set(meta.type, meta)
}
渲染器递归遍历节点,先查注册表、校验属性,再渲染子节点。缺失组件、版本不兼容或数据异常时应显示可定位的降级节点,而不是让整个页面崩溃。编辑器可以额外提供拖拽占位、选中框等装饰,但这些状态不应写入运行时 Schema。
3.1 编辑器数据层与所见即所得
编辑器的内部状态可以把每次用户操作归档为不可变快照,并将快照压入历史栈。快照必须是经过校验的 Schema,而不是夹杂选中框、拖拽指针等编辑态临时字段。预览模块直接消费最新快照,并尽量复用线上渲染引擎,才能让编辑态和运行态保持一致。
快照天然支持撤销、重做、操作记录和故障复现;文档很大时可以改用命令日志加周期性快照,避免每次复制整棵树。无论采用哪种实现,历史记录都应有大小上限,并在发布前固定 Schema、物料版本和数据源版本。
3.2 逻辑层用标准接口和插槽组合
将工具栏、物料区、画布、属性面板、预览和发布等功能拆成带标准输入输出的模块,由 Layout 基座提供稳定的插槽和宿主 API。模块通过注册和插拔接入,彼此不直接依赖内部状态;不同角色只暴露被授权的模块,新增或移除一个功能区不会牵连整棵组件树。
来源:大促搭投实践&高可用性保障(简化版).pdf 第 5-6 页。
4. 属性面板与事件模型
属性面板由 propsSchema 驱动,避免为每个组件手写一套表单。编辑操作应生成可逆的命令:
type Command = {
do(): void
undo(): void
label: string
}
撤销/重做栈保存命令或不可变快照;大文档可以使用操作日志和定期快照,避免每次复制整棵树。事件动作建议使用受限的声明式列表,例如 navigate、setState、request,由运行时逐项校验和执行,禁止 eval 执行任意表达式。
4.1 画布分层和编排事件
复杂画布可以把职责分到不同层:底层负责根据 DSL 渲染,覆盖层处理右键和选中态,再上一层处理快捷键或辅助操作。各层通过明确的事件总线通信,避免让每个物料都监听全局 DOM 事件。事件总线应有命名空间、解绑机制和生命周期边界,不能演变成无法追踪的全局状态。
编排能力可按复杂度逐步开放。块级物料适合先实现拖拽、排序和选中;自由画布再增加缩放、旋转、辅助线和吸附。吸附计算应基于模型节点的边、中心线等几何信息,并在拖拽过程中节流;拖拽结束只提交一次可回放的命令。节点 ID 必须稳定且唯一,不能使用不可复现的随机值作为长期标识。
这类编辑器既可以使用队列和指针实现撤销/重做,也可以把操作转成命令对象;关键是让数据、视图和操作解耦,预览始终是 DSL 的纯函数结果。
来源:低代码可视化平台架构设计内丹.pdf 第 3-5、13-16 页。
5. 插件化边界
插件可以扩展物料、属性编辑器、数据源适配器和发布能力,但应通过明确的宿主 API 接入:
type LowCodePlugin = {
name: string
version: string
install(api: {
registerComponent(meta: ComponentMeta): void
registerDataSource(kind: string, handler: unknown): void
}): void
}
插件加载时检查版本兼容性和权限;插件不能直接改写核心状态或绕过 Schema 校验。第三方插件的网络请求、文件访问和代码执行能力应隔离在服务端或独立沙箱中。
插件化设计可以用三件套来审查边界:
- 插件底座:维护核心状态、生命周期和宿主能力。
- 插件接口:声明可调用的钩子、输入输出、版本和权限。
- 插件实现:只通过接口注册能力,不修改底座内部实现。
物料通常由 type 和资源引用组成。注册表负责校验类型唯一性、建立 type -> renderer 映射,并在运行时注入给渲染器;在线物料还需要签名、来源审核、懒加载、卸载和错误隔离。不要把任意组件路径或第三方代码直接放进页面 Schema。
来源:低代码可视化平台架构设计内丹.pdf 第 7-12 页。
6. 数据绑定与安全
数据绑定要明确数据流方向:查询结果进入组件 props,表单事件产生受控变更,再由动作层提交。服务端必须重新校验查询参数、分页上限和资源权限。表达式只允许访问显式上下文(如 row、form、route),采用解释器或预编译白名单,不要直接 new Function。
页面发布时固定 Schema、物料版本和数据源配置,生成不可变发布版本;预览、灰度和回滚都以版本为单位。审计记录谁修改了哪些节点、何时发布以及使用了哪些插件。
7. 性能与测试
- 大画布使用虚拟化、局部渲染和节流后的拖拽更新;不要在每个鼠标移动事件中序列化整份 Schema。
- 运行时按页面或物料拆分代码,组件注册表可懒加载。
- 以 Schema fixture 做渲染快照和迁移测试,以交互测试覆盖选中、复制、撤销、数据加载和错误降级。
- 对发布产物做 Schema、权限和资源大小检查,防止不可运行页面进入生产。
7.1 出码、版本和协同扩展
“出码”本质上是把经过校验的 Schema 输出为可部署的 JSON 或渲染产物。发布记录应绑定 Schema 版本、物料版本、数据源和插件清单,预览、灰度和回滚都以不可变版本为单位。
历史记录、模板、分享和主题属于围绕版本的基础能力;协同编辑、流程图逻辑、定时任务和微前端集成属于进阶扩展。协同方案可按冲突模型选择操作转换或 CRDT(如 Yjs),但必须先定义节点 ID、权限和离线合并规则,不能只引入算法名。
来源:低代码可视化平台架构设计内丹.pdf 第 4-5 页。
8. 设计取舍
自由度越高,Schema 校验、迁移、权限和调试成本越高。企业场景通常优先选择受控物料目录和少量声明式动作,再逐步开放扩展点;“能拖出来”不是平台质量指标,可维护性和可回滚性才是。