从零手写低代码渲染引擎:核心原理与实战解析
一、引言
"低代码"这个词汇在近两三年几乎成了软件开发领域最热门的标签之一。然而,在这股热潮背后,真正理解低代码平台底层原理的开发者却并不多。很多人用过低代码平台,但面对"How does it work?"这个问题时,往往只能给出"拖拽生成代码"这样的模糊答案。
实际上,低代码平台远不止"生成代码"这么简单。它背后涉及Schema驱动渲染、组件工厂模式、响应式数据流、插件化架构等多个核心技术领域。
本文将从零开始,手写一个可运行的低代码渲染引擎。我们不只讲理论,更会给出可执行的代码,带你深入理解低代码平台的血管与骨骼。读完这篇文章,你将能够:
- 理解低代码平台最核心的 JSON Schema 驱动渲染 原理
- 掌握 组件注册与工厂模式 的实现方法
- 学会设计 表达式引擎 和 条件渲染 机制
- 了解 事件系统 与 数据联动 的实现思路
- 最终组装成一个可用的 迷你低代码渲染器
本文适合有一定前端基础(React/Vue)、对低代码技术栈感兴趣的开发者阅读。预计需要 20-30 分钟完整阅读。
二、低代码渲染引擎的核心架构
在开始动手之前,我们需要先理解低代码渲染引擎的整体架构。一个典型的低代码平台包含以下核心层级:
2.1 整体架构分层
┌─────────────────────────────────┐
│ 可视化设计器 (Designer) │
├─────────────────────────────────┤
│ JSON Schema 生成器 │
├─────────────────────────────────┤
│ 渲染引擎 (Renderer) │ ← 本文重点
├─────────────────────────────────┤
│ 组件库 (Component Lib) │
├─────────────────────────────────┤
│ 运行时框架 (React/Vue) │
└─────────────────────────────────┘
- 可视化设计器:用户拖拽组件、配置属性的界面
- JSON Schema 生成器:将可视化的配置序列化为结构化的 JSON 描述
- 渲染引擎(本文重点):接收 JSON Schema,动态渲染出真实 UI
- 组件库:可复用的 UI 组件集合
- 运行时框架:底层的前端框架(React、Vue 等)
2.2 JSON Schema 设计
低代码渲染引擎的核心输入是一个 UI 描述协议。我们把它设计成 JSON 格式,描述页面由哪些组件构成、如何布局、组件之间如何交互。
一个最小化的 Schema 结构如下:
{
"version": "1.0",
"components": [
{
"id": "page_root",
"type": "Page",
"props": {
"style": { "padding": "16px" }
},
"children": [
{
"id": "header_text",
"type": "Text",
"props": {
"content": "Hello, Low-Code!",
"fontSize": 24,
"color": "#333"
}
},
{
"id": "user_form",
"type": "Form",
"props": {
"title": "用户信息",
"labelWidth": 100
},
"children": [
{
"id": "name_input",
"type": "Input",
"props": {
"label": "姓名",
"placeholder": "请输入姓名",
"value": "",
"required": true
}
},
{
"id": "age_input",
"type": "Input",
"props": {
"label": "年龄",
"placeholder": "请输入年龄",
"value": "",
"required": false
}
},
{
"id": "submit_btn",
"type": "Button",
"props": {
"text": "提交",
"type": "primary",
"events": {
"onClick": "handleSubmit"
}
}
}
]
}
]
}
]
}
这个 Schema 到底有什么讲究?
每个节点必须具备:
- id:唯一标识,用于数据绑定和事件定位
- type:组件类型,渲染引擎据此查找已注册的组件
- props:组件的属性配置,驱动组件渲染行为
可选字段:
- children:子组件数组,构成组件树
- conditions:条件渲染规则
- dataBinding:数据绑定声明
2.3 渲染引擎的任务拆解
基于上面的 Schema,渲染引擎需要完成以下任务:
- Schema 解析:将 JSON 解析为内存中的组件树
- 组件查找:根据
type从组件注册表中找到对应的渲染组件 - 属性注入:将
props传递给组件,处理默认值和类型转换 - 递归渲染:遍历
children,递归渲染子组件 - 上下文传递:管理数据上下文、事件上下文、样式上下文
- 生命周期管理:组件的挂载、更新、卸载
三、组件注册与管理
渲染引擎的第一步是管理可用的组件。我们需要一个全局的组件注册表,让引擎知道给定 type: "Button" 时,应该渲染哪个实际的 React/Vue 组件。
为什么需要"注册"而不是直接 import?这里有一个重要的架构决策:渲染引擎不能硬编码依赖任何具体组件。想象一下:
- 你的引擎被两个不同项目使用,项目 A 用 Ant Design,项目 B 用 Material UI
- 项目 A 需要注册
Button→ Ant Design 的 Button,项目 B 需要注册Button→ Material UI 的 Button - 将来增加新组件(比如 Chart 图表组件)时,不需要修改引擎代码
这就是依赖倒置原则——渲染引擎依赖抽象(类型字符串和接口),不依赖具体实现(具体的 UI 组件包)。这个原则贯穿整个低代码架构设计。
3.1 组件注册表设计
// types.ts - 类型定义
interface ComponentMeta {
type: string; // 组件类型标识
component: any; // 实际的渲染组件
version?: string; // 组件版本
defaultProps?: Record<string, any>; // 默认属性
description?: string; // 组件描述
}
interface ComponentRegistry {
[type: string]: ComponentMeta;
}
3.2 注册表实现
// registry.ts - 组件注册表
class ComponentManager {
private registry: ComponentRegistry = {};
private static instance: ComponentManager;
// 单例模式
static getInstance(): ComponentManager {
if (!ComponentManager.instance) {
ComponentManager.instance = new ComponentManager();
}
return ComponentManager.instance;
}
// 注册组件
register(meta: ComponentMeta): void {
if (this.registry[meta.type]) {
console.warn(`Component type "${meta.type}" already registered. Overriding.`);
}
this.registry[meta.type] = meta;
}
// 批量注册
registerBatch(metas: ComponentMeta[]): void {
metas.forEach(meta => this.register(meta));
}
// 获取组件
get(type: string): ComponentMeta | undefined {
return this.registry[type];
}
// 检查组件是否已注册
has(type: string): boolean {
return type in this.registry;
}
// 获取所有已注册类型
getRegisteredTypes(): string[] {
return Object.keys(this.registry);
}
// 注销组件
unregister(type: string): void {
delete this.registry[type];
}
}
export const componentManager = ComponentManager.getInstance();
3.3 注册组件
有了注册表,我们就可以把实际的 UI 组件注册进去了。以 React 为例:
// components/Button.tsx
import React from 'react';
interface ButtonProps {
text: string;
type?: 'primary' | 'default' | 'danger';
onClick?: () => void;
style?: React.CSSProperties;
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({ text, type = 'default', onClick, style, disabled }) => {
const baseStyle: React.CSSProperties = {
padding: '8px 16px',
border: '1px solid #d9d9d9',
borderRadius: 4,
cursor: disabled ? 'not-allowed' : 'pointer',
fontSize: 14,
opacity: disabled ? 0.6 : 1,
...style
};
const typeStyles: Record<string, React.CSSProperties> = {
primary: { backgroundColor: '#1890ff', color: '#fff', borderColor: '#1890ff' },
default: { backgroundColor: '#fff', color: '#333' },
danger: { backgroundColor: '#ff4d4f', color: '#fff', borderColor: '#ff4d4f' }
};
return (
<button
style={{ ...baseStyle, ...typeStyles[type] }}
onClick={disabled ? undefined : onClick}
disabled={disabled}
>
{text}
</button>
);
};
export default Button;
// register-defaults.ts
import { componentManager } from './registry';
import Button from './components/Button';
import Text from './components/Text';
import Input from './components/Input';
import Form from './components/Form';
import Page from './components/Page';
// 注册内置组件
componentManager.registerBatch([
{ type: 'Button', component: Button, defaultProps: { type: 'default', text: '按钮' } },
{ type: 'Text', component: Text, defaultProps: { content: '', fontSize: 14, color: '#333' } },
{ type: 'Input', component: Input, defaultProps: { placeholder: '请输入', value: '' } },
{ type: 'Form', component: Form, defaultProps: { title: '', labelWidth: 80 } },
{ type: 'Page', component: Page, defaultProps: { style: {} } },
]);
这种注册机制有两大好处:
- 解耦:渲染引擎不依赖具体组件,只依赖注册表接口
- 可扩展:任何人都可以注册自定义组件,引擎无需修改
四、Schema 解析与递归渲染
有了组件注册表,接下来就是渲染引擎的核心——接收 Schema 并渲染出 UI 树。
4.1 渲染引擎实现
我们的渲染引擎以一个 Renderer 组件的形式存在,接收 Schema JSON 作为参数:
// Renderer.tsx
import React, { useContext } from 'react';
import { componentManager } from './registry';
import { RendererContext } from './RendererContext';
import ConditionEvaluator from './condition';
import ExpressionEngine from './expression';
interface SchemaNode {
id: string;
type: string;
props?: Record<string, any>;
children?: SchemaNode[];
conditions?: {
visible?: ConditionRule[];
disabled?: ConditionRule[];
};
}
interface RendererProps {
schema: {
version?: string;
components: SchemaNode[];
};
dataSource?: Record<string, any>;
eventHandler?: Record<string, Function>;
setDataSource?: (updater: any) => void;
}
const Renderer: React.FC<RendererProps> = ({
schema,
dataSource = {},
eventHandler = {},
setDataSource
}) => {
return (
<RendererContext.Provider value={{ dataSource, eventHandler, setDataSource }}>
{schema.components.map(node => (
<NodeRenderer key={node.id} node={node} />
))}
</RendererContext.Provider>
);
};
// 递归渲染单个节点
const NodeRenderer: React.FC<{ node: SchemaNode }> = React.memo(({ node }) => {
const context = useContext(RendererContext);
const meta = componentManager.get(node.type);
// 1. 组件未注册 → 降级显示
if (!meta) {
console.warn(`Component type "${node.type}" not registered. Falling back.`);
return <FallbackComponent node={node} />;
}
// 2. 检查条件渲染(Visible 条件)
if (node.conditions?.visible) {
const show = ConditionEvaluator.evaluate(node.conditions.visible, context.dataSource);
if (!show) return null;
}
// 3. 合并默认属性 + 用户配置属性
const mergedProps = {
...meta.defaultProps,
...node.props,
};
// 4. 处理 disabled 条件
if (node.conditions?.disabled) {
if (ConditionEvaluator.evaluate(node.conditions.disabled, context.dataSource)) {
mergedProps.disabled = true;
}
}
// 5. 处理属性中的表达式绑定
for (const key of Object.keys(mergedProps)) {
const value = mergedProps[key];
if (typeof value === 'string' && value.includes('{{')) {
mergedProps[key] = ExpressionEngine.resolveBindings(value, context.dataSource);
}
}
// 6. 处理事件配置
const eventProps: Record<string, Function> = {};
if (node.props?.events) {
for (const [eventName, eventConfig] of Object.entries(node.props.events)) {
eventProps[eventName] = (...args: any[]) => {
if (eventConfig.handler && context.eventHandler[eventConfig.handler]) {
context.eventHandler[eventConfig.handler](...args);
}
if (eventConfig.actions) {
ActionDispatcher.dispatch(eventConfig.actions, {
dataSource: context.dataSource,
setDataSource: context.setDataSource || (() => {}),
eventHandler: context.eventHandler,
});
}
};
}
}
// 7. 递归渲染子节点
const Component = meta.component;
const children = node.children?.map(child => (
<NodeRenderer key={child.id} node={child} />
));
return <Component {...mergedProps} {...eventProps}>{children}</Component>;
});
这段代码虽然只有几十行,但实现了渲染引擎最核心的五个能力:
- 动态组件查找:根据
type从注册表找组件 - 条件渲染:支持 visible/disabled 条件
- 属性合并:默认属性 + 用户配置属性
- 表达式解析:
{{xxx}}数据绑定 - 递归渲染:
children数组 → 子节点递归
4.2 上下文传递
渲染过程中,子组件往往需要访问父级的数据和全局事件。我们通过 React Context 实现:
// RendererContext.ts
import React from 'react';
interface RendererContextValue {
dataSource: Record<string, any>;
eventHandler: Record<string, Function>;
setDataSource?: (updater: any) => void;
}
export const RendererContext = React.createContext<RendererContextValue>({
dataSource: {},
eventHandler: {},
});
4.3 降级渲染
当注册表中找不到对应组件时,需要一个降级方案,避免整个页面白屏:
// FallbackComponent.tsx
const FallbackComponent: React.FC<{ node: SchemaNode }> = ({ node }) => {
return (
<div style={{
border: '1px dashed #ff4d4f',
padding: 12,
margin: 4,
borderRadius: 4,
backgroundColor: '#fff2f0',
fontSize: 12,
color: '#999'
}}>
<div style={{ fontWeight: 'bold', color: '#ff4d4f' }}>
⚠ 未知组件: {node.type}
</div>
<div style={{ marginTop: 4 }}>
ID: {node.id}
</div>
{node.props && (
<pre style={{ marginTop: 4, fontSize: 11, whiteSpace: 'pre-wrap' }}>
{JSON.stringify(node.props, null, 2)}
</pre>
)}
</div>
);
};
4.4 组件实现示例
有了引擎,我们来实现几个核心组件:
// components/Input.tsx
import React from 'react';
interface InputProps {
label: string;
placeholder: string;
value: string;
required: boolean;
onChange?: (value: string) => void;
style?: React.CSSProperties;
disabled?: boolean;
}
const Input: React.FC<InputProps> = ({
label, placeholder, value, required,
onChange, style, disabled
}) => {
return (
<div style={{ marginBottom: 12, display: 'flex', alignItems: 'center' }}>
{label && (
<label style={{
minWidth: 80, fontSize: 14, color: '#333',
flexShrink: 0
}}>
{required && <span style={{ color: 'red', marginRight: 4 }}>*</span>}
{label}
</label>
)}
<input
style={{
flex: 1, height: 32, padding: '0 8px',
border: '1px solid #d9d9d9', borderRadius: 4,
fontSize: 14, outline: 'none',
backgroundColor: disabled ? '#f5f5f5' : '#fff',
...style
}}
placeholder={placeholder}
value={value}
onChange={e => onChange?.(e.target.value)}
disabled={disabled}
/>
</div>
);
};
export default Input;
// components/Text.tsx
import React from 'react';
interface TextProps {
content: string;
fontSize?: number;
color?: string;
style?: React.CSSProperties;
}
const Text: React.FC<TextProps> = ({ content, fontSize = 14, color = '#333', style }) => {
return (
<span style={{ fontSize, color, ...style }}>
{content}
</span>
);
};
export default Text;
// components/Form.tsx
import React from 'react';
interface FormProps {
title: string;
labelWidth?: number;
children: React.ReactNode;
style?: React.CSSProperties;
}
const Form: React.FC<FormProps> = ({ title, labelWidth, children, style }) => {
return (
<div style={{
background: '#fff',
borderRadius: 8,
padding: 20,
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
...style
}}>
{title && (
<div style={{
fontSize: 16, fontWeight: 600, color: '#333',
paddingBottom: 12, marginBottom: 16,
borderBottom: '1px solid #f0f0f0'
}}>
{title}
</div>
)}
{children}
</div>
);
};
export default Form;
4.5 效果演示
用我们的引擎渲染一个简单的 Schema:
// App.tsx
import React, { useState } from 'react';
import Renderer from './Renderer';
import { useDataSource } from './useDataSource';
import './register-defaults';
const schema = {
version: "1.0",
components: [
{
id: "page_root",
type: "Page",
props: { style: { padding: "16px", maxWidth: 600, margin: "0 auto" } },
children: [
{
id: "header_text",
type: "Text",
props: {
content: "用户注册表单",
fontSize: 24, color: "#333",
style: { textAlign: "center", marginBottom: 24 }
}
},
{
id: "user_form",
type: "Form",
props: { title: "基本信息", labelWidth: 80 },
children: [
{ id: "name", type: "Input", props: { label: "姓名", placeholder: "请输入姓名", required: true } },
{ id: "email", type: "Input", props: { label: "邮箱", placeholder: "请输入邮箱", required: true } },
{ id: "phone", type: "Input", props: { label: "手机号", placeholder: "请输入手机号" } },
{
id: "submit",
type: "Button",
props: { text: "注册", type: "primary", style: { marginTop: 16 } }
}
]
}
]
}
]
};
function App() {
const { dataSource, setDataSource } = useDataSource({});
return (
<Renderer
schema={schema}
dataSource={dataSource}
setDataSource={setDataSource}
/>
);
}
现在,我们拥有了一个能渲染 Schema 的低代码引擎,但还缺少几个关键能力:表达式引擎、条件渲染、事件系统和数据联动。下面我们逐一实现。
五、表达式引擎
低代码平台离不开动态表达式。用户需要配置诸如"当 A 组件的值等于 X 时,B 组件显示"这样的逻辑。
5.1 表达式语法设计
我们设计一套轻量级的表达式语法,在运行时解析执行:
# 数据引用
{{dataSource.name}} → 获取数据源中的 name 字段
{{dataSource.user.age}} → 嵌套路径访问
# 比较运算
{{dataSource.age > 18}} → 年龄大于 18
{{dataSource.role === "admin"}} → 角色是管理员
# 逻辑组合
{{dataSource.age > 18 && dataSource.verified}}
{{dataSource.role === "admin" || dataSource.role === "super"}}
# 三元表达式
{{dataSource.gender === "male" ? "先生" : "女士"}}
# 内置函数
{{formatDate(dataSource.birthday, "YYYY-MM-DD")}}
{{concat(dataSource.firstName, " ", dataSource.lastName)}}
5.2 表达式解析器实现
// expression.ts - 表达式引擎
class ExpressionEngine {
// 解析数据绑定表达式:{{xxx}}
static resolveBindings(template: string, context: Record<string, any>): string {
return template.replace(/\{\{(.+?)\}\}/g, (match, expression) => {
try {
const value = this.evaluateExpression(expression.trim(), context);
return value !== undefined && value !== null ? String(value) : match;
} catch (e) {
console.warn(`[ExpressionEngine] Failed to resolve: ${expression}`, e);
return match;
}
});
}
// 评估布尔表达式(用于条件渲染)
static evaluateBoolean(expression: string, context: Record<string, any>): boolean {
try {
return Boolean(this.evaluateExpression(expression, context));
} catch {
return false;
}
}
// 根据路径从对象中取值
private static getValueByPath(obj: any, path: string): any {
if (!obj || !path) return undefined;
const keys = path.split('.');
let current = obj;
for (const key of keys) {
if (current === null || current === undefined) return undefined;
if (typeof current !== 'object') return undefined;
current = current[key];
}
return current;
}
// 安全执行表达式(沙箱模式)
private static evaluateExpression(expression: string, context: Record<string, any>): any {
// 如果是纯路径引用(不含运算符),直接路径取值
if (/^[a-zA-Z_$][a-zA-Z0-9_.]*$/.test(expression)) {
return this.getValueByPath(context, expression);
}
// 包含运算符 → 使用沙箱函数
// 通过 Proxy 拦截全局变量访问,只允许访问 context 中的数据
const sandbox = new Proxy(context, {
has: () => true, // 避免 "xxx is not defined" 报错
get: (target, key) => {
if (key === Symbol.unscopables) return undefined;
// 优先返回 context 中的值
if (key in target) return target[key as string];
// 路径取值辅助
return this.getValueByPath(target, key as string);
}
});
try {
const fn = new Function('sandbox', `
with (sandbox) {
return (${expression});
}
`);
return fn(sandbox);
} catch (e) {
console.warn(`[ExpressionEngine] Execution error: ${expression}`, e);
return undefined;
}
}
}
export default ExpressionEngine;
这里有个关键设计点:沙箱执行。我们使用 new Function + with + Proxy 构造了一个安全的作用域:
- Proxy 拦截属性访问,确保只能访问 context 中的数据结构
hastrap 返回true,避免with作用域中因为变量未定义而报错new Function动态创建函数执行表达式,比eval更安全
虽然这种沙箱并不能完全防止恶意代码(new Function 仍然能访问原型链),但对于低代码平台中可信的 Schema 配置来说已经足够。生产环境可以使用更严格的沙箱方案,如 iframe 或 vm2。
5.3 表达式在渲染中的应用
集成表达式解析到渲染器中:
// 在 NodeRenderer 中添加属性解析逻辑
function resolveProps(props: Record<string, any>, dataSource: Record<string, any>): Record<string, any> {
const resolved: Record<string, any> = {};
for (const [key, value] of Object.entries(props)) {
if (typeof value === 'string') {
resolved[key] = ExpressionEngine.resolveBindings(value, dataSource);
} else if (Array.isArray(value)) {
resolved[key] = value.map(item =>
typeof item === 'string' ? ExpressionEngine.resolveBindings(item, dataSource) : item
);
} else if (typeof value === 'object' && value !== null) {
resolved[key] = resolveProps(value, dataSource); // 递归处理嵌套对象
} else {
resolved[key] = value;
}
}
return resolved;
}
5.4 实用场景:表单默认值联动
表达式引擎最典型的应用场景是表单字段之间的联动。比如:
- 选择"是"选项后,弹出一个新的输入框
- 输入金额后,税率字段自动计算
- 根据用户选择的省份,自动填充城市下拉框
下面是一个实际 Schema 片段,展示了表达式如何实现字段联动:
{
"id": "order_form",
"type": "Form",
"props": { "title": "订单信息" },
"children": [
{
"id": "product_type",
"type": "Select",
"props": {
"label": "产品类型",
"options": [
{ "label": "电子产品", "value": "electronics" },
{ "label": "食品", "value": "food" },
{ "label": "服装", "value": "clothing" }
]
}
},
{
"id": "price_input",
"type": "Input",
"props": {
"label": "单价(元)",
"placeholder": "请输入单价"
}
},
{
"id": "quantity_input",
"type": "Input",
"props": {
"label": "数量",
"placeholder": "请输入数量",
"value": "1"
}
},
{
"id": "total_display",
"type": "Text",
"props": {
"content": "合计:{{dataSource.price_input * dataSource.quantity_input || 0}} 元",
"fontSize": 18,
"color": "#1890ff",
"style": { "fontWeight": "bold", "marginTop": 8 }
}
},
{
"id": "tax_info",
"type": "Text",
"props": {
"content": "(含税:{{(dataSource.price_input * dataSource.quantity_input * 0.13).toFixed(2)}} 元)",
"fontSize": 12,
"color": "#999"
}
}
]
}
这里,total_display 和 tax_info 都通过表达式引擎动态计算。当用户在 price_input 或 quantity_input 中输入数值时,合计和税额会发生联动更新。这一切都不需要写一行业务代码——完全通过 Schema 配置完成。这就是低代码真正的威力所在。
六、条件渲染
低代码平台中最常见的需求之一:根据条件控制组件的显示与隐藏。
6.1 条件规则 Schema
{
"id": "vip_badge",
"type": "Text",
"props": { "content": "VIP 会员专属", "color": "gold" },
"conditions": {
"visible": [
{ "field": "dataSource.userLevel", "operator": ">=", "value": 3 }
],
"disabled": [
{ "field": "dataSource.accountStatus", "operator": "===", "value": "frozen" }
]
}
}
6.2 条件求值器
// condition.ts
interface ConditionRule {
field: string; // 数据字段路径
operator: string; // 比较运算符
value: any; // 比较值
}
class ConditionEvaluator {
static evaluate(rules: ConditionRule[], data: Record<string, any>): boolean {
if (!rules || rules.length === 0) return true;
// 默认 AND 逻辑(多个条件需要同时满足)
return rules.every(rule => {
const fieldValue = this.getFieldValue(data, rule.field);
switch (rule.operator) {
case '===': return fieldValue === rule.value;
case '!==': return fieldValue !== rule.value;
case '>': return Number(fieldValue) > Number(rule.value);
case '>=': return Number(fieldValue) >= Number(rule.value);
case '<': return Number(fieldValue) < Number(rule.value);
case '<=': return Number(fieldValue) <= Number(rule.value);
case 'in': return Array.isArray(rule.value) && rule.value.includes(fieldValue);
case 'contains':
return String(fieldValue).includes(String(rule.value));
case 'isEmpty':
return fieldValue === '' || fieldValue === null || fieldValue === undefined;
case 'isNotEmpty':
return fieldValue !== '' && fieldValue !== null && fieldValue !== undefined;
case 'startsWith':
return String(fieldValue).startsWith(String(rule.value));
case 'endsWith':
return String(fieldValue).endsWith(String(rule.value));
case 'regex':
try {
return new RegExp(rule.value).test(String(fieldValue));
} catch {
return false;
}
default:
console.warn(`[ConditionEvaluator] Unknown operator: ${rule.operator}`);
return true;
}
});
}
static getFieldValue(data: any, field: string): any {
return field.split('.').reduce((obj, key) => obj?.[key], data);
}
}
export default ConditionEvaluator;
6.3 与渲染器集成
在 NodeRenderer 中,条件渲染的逻辑已经集成:
// 条件渲染(visible = false → 组件隐藏)
if (node.conditions?.visible) {
if (!ConditionEvaluator.evaluate(node.conditions.visible, context.dataSource)) {
return null;
}
}
// 条件禁用(disabled = true → 组件不可操作)
if (node.conditions?.disabled) {
if (ConditionEvaluator.evaluate(node.conditions.disabled, context.dataSource)) {
mergedProps.disabled = true;
}
}
6.4 实战场景:动态表单
条件渲染在表单场景中有大量应用。比如一个"是否开启高级选项"的开关:
{
"id": "basic_form",
"type": "Form",
"props": { "title": "用户配置" },
"children": [
{
"id": "enable_advanced",
"type": "Switch",
"props": {
"label": "开启高级设置",
"value": false
}
},
{
"id": "advanced_section",
"type": "Form",
"props": { "title": "高级设置", "style": { "marginTop": 16, "border": "1px solid #e8e8e8", "borderRadius": 8 } },
"conditions": {
"visible": [
{ "field": "dataSource.enable_advanced", "operator": "===", "value": true }
]
},
"children": [
{ "id": "api_endpoint", "type": "Input", "props": { "label": "API 地址", "placeholder": "https://api.example.com" } },
{ "id": "api_key", "type": "Input", "props": { "label": "API Key", "placeholder": "请输入密钥" } },
{
"id": "timeout_config",
"type": "Select",
"props": {
"label": "超时时间",
"options": [
{ "label": "5 秒", "value": 5000 },
{ "label": "10 秒", "value": 10000 },
{ "label": "30 秒", "value": 30000 }
]
}
}
]
}
]
}
当用户关闭"开启高级设置"开关时,整个"高级设置"区域会自动隐藏,表单看起来简洁清爽。开启后才会展开更多配置项。这种渐进式披露设计模式在复杂表单中非常实用,用户不会被海量选项吓到,按需展开即可。
另一个常见场景是:问卷调查中,选 A 则跳出隐藏的附加题,选 B 则跳过。在传统开发中,这需要写大量 if-else 条件渲染逻辑;而在低代码中,一个 conditions 配置就能搞定。
6.5 条件的逻辑组合
我们的条件求值器目前只支持 AND 逻辑(所有条件同时满足)。但实际场景中通常需要 OR(任一条件满足):
{
"conditions": {
"visible": {
"logic": "or",
"rules": [
{ "field": "dataSource.role", "operator": "===", "value": "admin" },
{ "field": "dataSource.role", "operator": "===", "value": "super_admin" },
{ "field": "dataSource.isOwner", "operator": "===", "value": true }
]
}
}
}
对应的求值器需要支持逻辑组合:
static evaluate(rules: ConditionRule[], data: Record<string, any>, logic: 'and' | 'or' = 'and'): boolean {
if (!rules || rules.length === 0) return true;
const results = rules.map(rule => this.evaluateSingle(rule, data));
if (logic === 'or') {
return results.some(Boolean); // 任一满足即可
}
return results.every(Boolean); // 全部满足才可
}
这种"逻辑组合"能力让条件渲染从简单的开关变成强大的业务规则引擎。
七、事件系统与数据联动
低代码平台真正的"魔法"不在于静态渲染,而在于组件之间的动态交互——点击按钮触发提交、输入内容联动其他组件、数据变化驱动 UI 更新。
7.1 事件系统设计
// event-system.ts
interface EventConfig {
handler?: string; // 事件处理器名称(外部注入)
actions?: ActionConfig[]; // 触发后的动作链
}
interface ActionConfig {
type: 'setValue' | 'setProps' | 'show' | 'hide'
| 'callApi' | 'navigate' | 'resetForm' | 'validateForm';
target?: string; // 目标组件 id
payload?: any; // 动作数据
}
7.2 Schema 事件配置示例
以经典的省市区联动为例:
{
"id": "province_select",
"type": "Select",
"props": {
"label": "省份",
"placeholder": "请选择省份",
"options": [
{ "label": "北京市", "value": "110000" },
{ "label": "上海市", "value": "310000" },
{ "label": "广东省", "value": "440000" },
{ "label": "浙江省", "value": "330000" }
],
"events": {
"onChange": {
"actions": [
{
"type": "setValue",
"target": "city_select",
"payload": {
"value": "",
"options": []
}
},
{
"type": "setValue",
"target": "district_select",
"payload": {
"value": "",
"options": []
}
},
{
"type": "callApi",
"payload": {
"url": "/api/regions/{{dataSource.province_select.value}}",
"method": "GET",
"onSuccess": {
"type": "setProps",
"target": "city_select",
"payload": {
"options": "{{dataSource.apiResult}}"
}
}
}
}
]
}
}
}
}
这个配置实现了完整的三级联动逻辑:
- 省份变化 → 清空城市和区县的值与选项
- 调用后端 API 获取该省份的城市列表
- 将 API 返回的数据设为城市下拉框的选项
7.3 动作分发器实现
// action-dispatcher.ts
class ActionDispatcher {
static async dispatch(
actions: ActionConfig[],
context: {
dataSource: Record<string, any>;
setDataSource: (updater: any) => void;
eventHandler: Record<string, Function>;
}
): Promise<void> {
for (const action of actions) {
await this.executeAction(action, context);
}
}
private static async executeAction(
action: ActionConfig,
context: {
dataSource: Record<string, any>;
setDataSource: (updater: any) => void;
eventHandler: Record<string, Function>;
}
): Promise<void> {
switch (action.type) {
case 'setValue':
context.setDataSource((prev: Record<string, any>) => ({
...prev,
[action.target!]: {
...(prev[action.target!] || {}),
...action.payload
}
}));
break;
case 'setProps':
context.setDataSource((prev: Record<string, any>) => ({
...prev,
[`__props_${action.target}`]: {
...(prev[`__props_${action.target}`] || {}),
...action.payload
}
}));
break;
case 'show':
context.setDataSource((prev: Record<string, any>) => ({
...prev,
[`__visible_${action.target}`]: true
}));
break;
case 'hide':
context.setDataSource((prev: Record<string, any>) => ({
...prev,
[`__visible_${action.target}`]: false
}));
break;
case 'resetForm':
// 重置目标表单的所有字段
context.setDataSource((prev: Record<string, any>) => {
const newData = { ...prev };
Object.keys(newData).forEach(key => {
if (key.startsWith(`${action.target}_`)) {
delete newData[key];
}
});
return newData;
});
break;
case 'callApi':
try {
// 解析模板化 URL 和参数
const resolvedUrl = ExpressionEngine.resolveBindings(
action.payload.url, context.dataSource
);
const response = await fetch(resolvedUrl, {
method: action.payload.method || 'GET',
headers: {
'Content-Type': 'application/json',
...(action.payload.headers || {})
}
});
const result = await response.json();
// 更新数据源
context.setDataSource((prev: Record<string, any>) => ({
...prev,
apiResult: result
}));
// 处理 onSuccess 回调动作
if (action.payload.onSuccess) {
await this.executeAction(action.payload.onSuccess, context);
}
} catch (error) {
console.error('[ActionDispatcher] API call failed:', error);
if (action.payload.onError) {
await this.executeAction(action.payload.onError, context);
}
}
break;
case 'navigate':
const url = ExpressionEngine.resolveBindings(
action.payload.url, context.dataSource
);
if (action.payload.target === '_blank') {
window.open(url, '_blank');
} else {
window.location.href = url;
}
break;
default:
console.warn(`[ActionDispatcher] Unknown action type: ${action.type}`);
}
}
}
export default ActionDispatcher;
7.4 useDataSource Hook
为了支持数据驱动的响应式更新,我们封装一个 Hook:
// useDataSource.ts
import { useState, useCallback } from 'react';
export function useDataSource(initialData: Record<string, any> = {}) {
const [dataSource, setDataSource] = useState(() => ({
...initialData
}));
// 更新单个字段(支持嵌套路径)
const updateField = useCallback((field: string, value: any) => {
setDataSource(prev => {
const keys = field.split('.');
const newData = { ...prev };
if (keys.length === 1) {
newData[keys[0]] = value;
} else {
let current = newData;
for (let i = 0; i < keys.length - 1; i++) {
current[keys[i]] = { ...(current[keys[i]] || {}) };
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
}
return newData;
});
}, []);
return { dataSource, setDataSource, updateField };
}
7
八、布局系统
一个真正的低代码引擎还需要布局能力。我们实现一套基于 Flexbox 的栅格布局系统:
8.1 布局容器组件
// components/Row.tsx
import React from 'react';
interface RowProps {
children: React.ReactNode;
gutter?: number; // 栅格间距
justify?: 'start' | 'center' | 'end' | 'space-between' | 'space-around';
align?: 'top' | 'middle' | 'bottom' | 'stretch';
style?: React.CSSProperties;
}
const alignMap: Record<string, string> = {
top: 'flex-start',
middle: 'center',
bottom: 'flex-end',
stretch: 'stretch'
};
const Row: React.FC<RowProps> = ({
children, gutter = 0,
justify = 'start', align = 'top', style
}) => {
return (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: justify,
alignItems: alignMap[align] || align,
marginLeft: -gutter / 2,
marginRight: -gutter / 2,
...style,
}}
>
{React.Children.map(children, child =>
child ? <div style={{ paddingLeft: gutter / 2, paddingRight: gutter / 2 }}>{child}</div> : null
)}
</div>
);
};
export default Row;
// components/Col.tsx
import React from 'react';
interface ColProps {
span?: number; // 栅格占位数(1-24)
offset?: number; // 左侧偏移栅格数
children?: React.ReactNode;
style?: React.CSSProperties;
}
const Col: React.FC<ColProps> = ({ span = 24, offset = 0, children, style }) => {
const widthPercent = (span / 24) * 100;
const offsetPercent = (offset / 24) * 100;
return (
<div
style={{
width: `${widthPercent}%`,
marginLeft: `${offsetPercent}%`,
boxSizing: 'border-box',
...style,
}}
>
{children}
</div>
);
};
export default Col;
这套栅格系统参考了 Ant Design 的 Layout 设计,但实现更轻量:
- 24 栅格系统:
span取值 1-24,24 代表整行 - Flexbox 实现:天然支持响应式换行和弹性布局
gutter间距:通过负 margin + padding 实现,避免破坏 Flex 子项宽度计算
8.2 布局 Schema 示例
{
"id": "dashboard_page",
"type": "Page",
"props": { "style": { "padding": 24, "background": "#f0f2f5" } },
"children": [
{
"id": "header_row",
"type": "Row",
"props": { "justify": "space-between", "align": "middle", "gutter": 16 },
"children": [
{
"id": "title_col",
"type": "Col",
"props": { "span": 12 },
"children": [
{ "id": "page_title", "type": "Text", "props": { "content": "数据看板", "fontSize": 20, "color": "#333", "style": { "fontWeight": "bold" } } }
]
},
{
"id": "actions_col",
"type": "Col",
"props": { "span": 12, "style": { "textAlign": "right" } },
"children": [
{ "id": "refresh_btn", "type": "Button", "props": { "text": "刷新数据", "type": "primary" } },
{ "id": "export_btn", "type": "Button", "props": { "text": "导出报表", "type": "default", "style": { "marginLeft": 8 } } }
]
}
]
},
{
"id": "stats_row",
"type": "Row",
"props": { "gutter": 16 },
"children": [
{ "id": "card1", "type": "Col", "props": { "span": 6 }, "children": [{ "id": "stat1", "type": "StatCard", "props": { "title": "总用户数", "value": "128,456", "icon": "users" } }] },
{ "id": "card2", "type": "Col", "props": { "span": 6 }, "children": [{ "id": "stat2", "type": "StatCard", "props": { "title": "今日活跃", "value": "3,842", "icon": "activity" } }] },
{ "id": "card3", "type": "Col", "props": { "span": 6 }, "children": [{ "id": "stat3", "type": "StatCard", "props": { "title": "总收入", "value": "¥89,231", "icon": "dollar" } }] },
{ "id": "card4", "type": "Col", "props": { "span": 6 }, "children": [{ "id": "stat4", "type": "StatCard", "props": { "title": "转化率", "value": "3.24%", "icon": "trending" } }] }
]
}
]
}
九、性能优化
低代码渲染引擎有其特殊的性能挑战:Schema 可能很大(数百个节点),数据变化频繁,需要精细控制渲染范围。
9.1 使用 React.memo 避免过度渲染
const NodeRenderer = React.memo(
({ node, depth = 0 }: { node: SchemaNode; depth?: number }) => {
// ... 渲染逻辑
},
(prevProps, nextProps) => {
return prevProps.node === nextProps.node;
}
);
通过 React.memo + 引用比较,避免父组件重新渲染时递归引起全网状更新。
然而,引号字面量比较有一个问题:如果 Schema 每次都重新创建(如写在组件内联),引用比较会失效。因此需要稳定化 Schema 引用:
// 在顶层定义 Schema
const schema = { /* ... */ }; // 模块级常量
// 或使用 useRef
const schemaRef = useRef(schemaObj);
9.2 Schema 缓存机制
当一个 Schema 很大时,解析和遍历是昂贵的。我们可以缓存已解析的虚拟节点树:
// schema-cache.ts
import { createHash } from 'crypto';
class SchemaCache {
private cache = new Map<string, {
parsedTree: any;
hash: string;
timestamp: number;
}>();
private maxSize = 50; // 最多缓存 50 个 Schema
get(key: string, schema: any): any | null {
const hash = this.computeHash(schema);
const cached = this.cache.get(key);
if (cached && cached.hash === hash) {
return cached.parsedTree;
}
return null;
}
set(key: string, schema: any, parsedTree: any): void {
// LRU 清理:超出容量时删除最旧的
if (this.cache.size >= this.maxSize) {
const oldestKey = this.cache.keys().next().value;
if (oldestKey) this.cache.delete(oldestKey);
}
this.cache.set(key, {
parsedTree,
hash: this.computeHash(schema),
timestamp: Date.now(),
});
}
private computeHash(obj: any): string {
return JSON.stringify(obj);
}
invalidate(key: string): void {
this.cache.delete(key);
}
clear(): void {
this.cache.clear();
}
}
export const schemaCache = new SchemaCache();
9.3 组件懒加载
对于大型低代码应用,一次性加载所有组件会导致首屏过慢:
// lazy-registry.ts
import { componentManager } from './registry';
class LazyComponentManager {
private loading = new Map<string, Promise<void>>();
async ensureLoaded(type: string): Promise<void> {
if (componentManager.has(type)) return;
if (this.loading.has(type)) {
await this.loading.get(type);
return;
}
const promise = this.loadComponent(type);
this.loading.set(type, promise);
try {
await promise;
} catch (error) {
this.loading.delete(type);
throw error;
}
}
private async loadComponent(type: string): Promise<void> {
// 按照约定路径加载组件
// 例如 type="Chart" → ./dynamic/Chart/index.tsx
const module = await import(
/* webpackChunkName: "lc-[request]" */
`./dynamic/${type}/index.tsx`
);
componentManager.register({
type,
component: module.default,
description: `Lazy-loaded component: ${type}`,
});
}
}
export const lazyComponentManager = new LazyComponentManager();
使用方式:
// 在 NodeRenderer 中集成懒加载
const NodeRenderer: React.FC<{ node: SchemaNode }> = ({ node }) => {
const [loaded, setLoaded] = useState(componentManager.has(node.type));
useEffect(() => {
if (!componentManager.has(node.type)) {
lazyComponentManager.ensureLoaded(node.type).then(() => {
setLoaded(true);
});
}
}, [node.type]);
if (!loaded) {
return <div style={{ height: 50, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999' }}>
正在加载组件 {node.type}...
</div>;
}
// ... 正常渲染
};
9.4 细粒度数据订阅
当 dataSource 变化时,不需要重新渲染所有组件。可以使用 useSyncExternalStore 或 Proxy 实现细粒度的数据订阅:
// reactive-store.ts
class ReactiveStore {
private data: Record<string, any>;
private listeners = new Map<string, Set<() => void>>();
constructor(initial: Record<string, any> = {}) {
this.data = { ...initial };
}
get(path: string): any {
return path.split('.').reduce((obj, key) => obj?.[key], this.data);
}
set(path: string, value: any): void {
const oldValue = this.get(path);
if (oldValue === value) return;
// 更新数据
const keys = path.split('.');
let current = this.data;
for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]] || typeof current[keys[i]] !== 'object') {
current[keys[i]] = {};
}
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
// 触发订阅
this.notify(path);
}
subscribe(path: string, listener: () => void): () => void {
if (!this.listeners.has(path)) {
this.listeners.set(path, new Set());
}
this.listeners.get(path)!.add(listener);
return () => {
this.listeners.get(path)?.delete(listener);
};
}
private notify(path: string): void {
// 通知精确路径的订阅者
this.listeners.get(path)?.forEach(fn => fn());
// 通知祖先路径的订阅者(如监听 "user" 的人,也需要知道 "user.name" 的变化)
const parts = path.split('.');
for (let i = parts.length - 1; i > 0; i--) {
const ancestorPath = parts.slice(0, i).join('.');
this.listeners.get(ancestorPath)?.forEach(fn => fn());
}
// 通知全量订阅(监听 '' 或 '*' 的人)
this.listeners.get('*')?.forEach(fn => fn());
}
}
十、完整示例:构建一个数据看板
让我们把所有组件组合起来,构建一个完整的 数据看板页面。
10.1 完整 Schema
{
"version": "1.0",
"components": [
{
"id": "page",
"type": "Page",
"props": { "style": { "padding": "24px", "background": "#f0f2f5", "minHeight": "100vh" } },
"children": [
{
"id": "header_row",
"type": "Row",
"props": { "justify": "space-between", "align": "middle" },
"children": [
{
"id": "title_col",
"type": "Col",
"props": { "span": 12 },
"children": [
{
"id": "title",
"type": "Text",
"props": {
"content": "运营数据看板",
"fontSize": 24,
"color": "#1a1a1a",
"style": { "fontWeight": "bold" }
}
},
{
"id": "subtitle",
"type": "Text",
"props": {
"content": "实时数据更新于 {{dataSource.lastUpdate}}",
"fontSize": 13,
"color": "#999",
"style": { "marginTop": 4 }
}
}
]
},
{
"id": "actions_col",
"type": "Col",
"props": { "span": 12, "style": { "textAlign": "right" } },
"children": [
{
"id": "refresh_btn",
"type": "Button",
"props": {
"text": "刷新数据",
"type": "primary",
"events": {
"onClick": {
"actions": [
{
"type": "setValue",
"target": "lastUpdate",
"payload": { "value": "刷新中..." }
},
{
"type": "callApi",
"payload": {
"url": "/api/dashboard",
"method": "GET",
"onSuccess": {
"type": "setValue",
"target": "dashboardData",
"payload": { "value": "{{dataSource.apiResult}}" }
}
}
},
{
"type": "setValue",
"target": "lastUpdate",
"payload": { "value": "{{new Date().toLocaleString()}}" }
}
]
}
}
}
}
]
}
]
},
{
"id": "stats_row",
"type": "Row",
"props": { "gutter": 16, "style": { "marginTop": 24 } },
"children": [
{ "id": "col1", "type": "Col", "props": { "span": 6 }, "children": [{ "id": "card1", "type": "StatCard", "props": { "title": "DAU", "value": "{{dataSource.dau}}", "color": "#1890ff" } }] },
{ "id": "col2", "type": "Col", "props": { "span": 6 }, "children": [{ "id": "card2", "type": "StatCard", "props": { "title": "订单量", "value": "{{dataSource.orders}}", "color": "#52c41a" } }] },
{ "id": "col3", "type": "Col", "props": { "span": 6 }, "children": [{ "id": "card3", "type": "StatCard", "props": { "title": "营收 (元)", "value": "¥{{dataSource.revenue}}", "color": "#faad14" } }] },
{ "id": "col4", "type": "Col", "props": { "span": 6 }, "children": [{ "id": "card4", "type": "StatCard", "props": { "title": "转化率", "value": "{{dataSource.conversion}}%", "color": "#ff4d4f" } }] }
]
},
{
"id": "content_row",
"type": "Row",
"props": { "gutter": 16, "style": { "marginTop": 24 } },
"children": [
{
"id": "chart_col",
"type": "Col",
"props": { "span": 16 },
"children": [
{
"id": "trend_chart",
"type": "Form",
"props": { "title": "近 7 日趋势" },
"children": [
{ "id": "chart_placeholder", "type": "Text", "props": { "content": "📊 趋势图渲染区域", "fontSize": 14, "color": "#666" } }
]
}
]
},
{
"id": "alert_col",
"type": "Col",
"props": { "span": 8 },
"children": [
{
"id": "alert_list",
"type": "Form",
"props": { "title": "告警通知" },
"conditions": {
"visible": [
{ "field": "dataSource.hasAlerts", "operator": "===", "value": true }
]
},
"children": [
{
"id": "no_alert",
"type": "Text",
"props": { "content": "✅ 所有系统运行正常", "fontSize": 14, "color": "#52c41a" },
"conditions": {
"visible": [
{ "field": "dataSource.alertCount", "operator": "===", "value": 0 }
]
}
}
]
}
]
}
]
}
]
}
]
}
这个 Schema 演示了一个企业级数据看板的所有关键特性:
- 响应式布局:通过 Row/Col 系统实现
- 数据绑定:使用
{{dataSource.xxx}}语法动态取值 - 事件联动:刷新按钮 → API 调用 → 更新数据
- 条件渲染:告警列表根据是否有告警动态显示
- 表达式动态内容:时间戳、格式化数字等
10.2 组装渲染
// DashboardApp.tsx
import React, { useEffect, useState } from 'react';
import Renderer from './Renderer';
import { useDataSource } from './useDataSource';
import './register-defaults';
import dashboardSchema from './schemas/dashboard.json';
const DashboardApp: React.FC = () => {
const { dataSource, setDataSource } = useDataSource({
dau: '--',
orders: '--',
revenue: '--',
conversion: '--',
hasAlerts: false,
alertCount: 0,
lastUpdate: new Date().toLocaleString(),
});
// 初始化加载数据
useEffect(() => {
fetch('/api/dashboard')
.then(res => res.json())
.then(result => {
setDataSource((prev: any) => ({
...prev,
...result,
lastUpdate: new Date().toLocaleString(),
}));
})
.catch(() => {
// 降级:使用模拟数据
setDataSource((prev: any) => ({
...prev,
dau: '128,456',
orders: '3,842',
revenue: '89,231.50',
conversion: '3.24',
hasAlerts: false,
alertCount: 0,
lastUpdate: new Date().toLocaleString(),
}));
});
}, []);
return (
<Renderer
schema={dashboardSchema}
dataSource={dataSource}
setDataSource={setDataSource}
/>
);
};
export default DashboardApp;
十一、工程化与扩展
一个生产级的低代码渲染引擎还需要考虑以下方面:
11.1 插槽系统 (Slot)
低代码组件的插槽机制允许用户自定义组件的某个区域,比如 Card 组件的"右上角操作区"和"底部区域":
{
"id": "my_card",
"type": "Card",
"props": {
"title": "用户卡片",
"slots": {
"extra": [
{
"id": "more_btn",
"type": "Button",
"props": { "text": "更多", "type": "link" }
}
],
"footer": [
{
"id": "footer_text",
"type": "Text",
"props": { "content": "底部信息", "color": "#999", "fontSize": 12 }
}
]
}
}
}
渲染引擎需要解析 slots 并在相应位置渲染:
// Card 组件中插槽渲染逻辑
const Card: React.FC<CardProps & { slots?: Record<string, SchemaNode[]> }> = ({
title, children, slots
}) => {
// slots 中的内容也会通过 NodeRenderer 递归渲染
const slotExtra = slots?.extra?.map(node => <NodeRenderer key={node.id} node={node} />);
const slotFooter = slots?.footer?.map(node => <NodeRenderer key={node.id} node={node} />);
return (
<div className="card">
<div className="card-header">
<span className="card-title">{title}</span>
<div className="card-extra">{slotExtra}</div>
</div>
<div className="card-body">{children}</div>
{slotFooter && <div className="card-footer">{slotFooter}</div>}
</div>
);
};
11.2 主题系统
通过 Context 实现主题切换:
// theme-context.ts
interface Theme {
primaryColor: string;
borderRadius: number;
fontSize: Record<string, number>;
spacing: Record<string, number>;
colorScheme: 'light' | 'dark';
}
const defaultTheme: Theme = {
primaryColor: '#1890ff',
borderRadius: 4,
fontSize: { small: 12, normal: 14, large: 16, xlarge: 20 },
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 },
colorScheme: 'light',
};
主题注入到 RendererContext 中,所有组件通过 useContext 获取主题配置后自动应用。这样,用户只需在 Schema 顶层配置一次主题,所有子组件都会自动响应。
11.3 Schema 验证
在生产环境中,Schema 需要验证,防止非法数据导致运行时错误:
// schema-validator.ts
interface ValidationError {
path: string;
message: string;
}
interface ValidationResult {
valid: boolean;
errors: ValidationError[];
}
class SchemaValidator {
static validate(schema: any): ValidationResult {
const errors: ValidationError[] = [];
// 1. 检查版本号
if (!schema.version) {
errors.push({ path: 'version', message: '缺少版本号' });
}
// 2. 检查 components 字段
if (!Array.isArray(schema.components)) {
errors.push({ path: 'components', message: 'components 必须是数组' });
return { valid: false, errors };
}
// 3. 递归检查每个节点
schema.components.forEach((node: any, index: number) => {
this.validateNode(node, `components[${index}]`, errors);
});
return { valid: errors.length === 0, errors };
}
private static validateNode(node: any, path: string, errors: ValidationError[]): void {
if (!node.id || typeof node.id !== 'string') {
errors.push({ path: `${path}.id`, message: '缺少 id 或 id 不是字符串' });
}
if (!node.type || typeof node.type !== 'string') {
errors.push({ path: `${path}.type`, message: '缺少 type 或 type 不是字符串' });
}
if (node.children) {
if (!Array.isArray(node.children)) {
errors.push({ path: `${path}.children`, message: 'children 必须是数组' });
} else {
node.children.forEach((child: any, i: number) => {
this.validateNode(child, `${path}.children[${i}]`, errors);
});
}
}
}
}
这一步骤虽然看似简单,但在实际生产环境中能避免大量线上问题。建议在解析 Schema 后、渲染前强制调用验证,并返回友好的错误提示。
11.4 国际化 (i18n) 方案
低代码平台面向多语言场景时,Schema 中不能写死文案。设计动态文本方案:
{
"id": "welcome_message",
"type": "Text",
"props": {
"content_i18n": {
"zh-CN": "欢迎使用低代码平台",
"en-US": "Welcome to Low-Code Platform",
"ja-JP": "ローコードプラットフォームへようこそ"
}
}
}
// i18n-resolver.ts
class I18nResolver {
private locale: string;
constructor(locale: string) {
this.locale = locale;
}
resolve(prop: any): any {
// 如果属性是 i18n 格式,根据当前语言返回对应文本
if (prop && typeof prop === 'object' && !Array.isArray(prop)) {
if (prop['zh-CN'] !== undefined || prop['en-US'] !== undefined) {
return prop[this.locale] || prop['zh-CN'] || prop['en-US'] || '';
}
}
return prop;
}
}
11.5 错误边界
渲染引擎要足够健壮,单个组件的渲染错误不能导致整个页面崩溃。React 的 ErrorBoundary 是最佳实践:
// ErrorBoundary.tsx
import React from 'react';
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback?: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
constructor(props: any) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error(`[Renderer] Component render error:`, error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div style={{
padding: 16,
border: '1px solid #ffccc7',
borderRadius: 4,
background: '#fff2f0',
color: '#ff4d4f',
fontSize: 13,
}}>
组件渲染异常: {this.state.error?.message}
</div>
);
}
return this.props.children;
}
}
// 在 NodeRenderer 中包裹
<ErrorBoundary key={node.id}>
<Component {...resolvedProps} {...eventProps}>
{children}
</Component>
</ErrorBoundary>
11.6 元数据系统
每个组件可以附带元数据描述,供设计器使用。这部分数据在渲染时不需要,但用于可视化编辑:
// component-meta.ts
interface ComponentMetaDefinition {
type: string;
title: string;
group: string; // 组件分组(布局、表单、展示等)
icon: string; // 组件图标
version: string;
description: string;
props: PropDefinition[]; // 属性编辑器定义
defaultProps: Record<string, any>;
snippets?: SchemaNode[]; // 组件模板片段
}
interface PropDefinition {
name: string;
title: string;
type: 'string' | 'number' | 'boolean' | 'select' | 'color' | 'json';
defaultValue?: any;
required?: boolean;
options?: { label: string; value: any }[];
description?: string;
}
这些元数据可以让可视化设计器自动生成属性面板,无需为每个组件手写配置表单。
11.7 撤消/重做 (Undo/Redo)
在设计器中操作 Schema 时,Undo/Redo 是必不可少的:
// history-manager.ts
interface HistoryEntry {
schema: any;
timestamp: number;
description: string;
}
class HistoryManager {
private stack: HistoryEntry[] = [];
private currentIndex = -1;
private maxSize = 50;
push(schema: any, description: string): void {
// 删除当前位置之后的历史(如果在中间执行新操作)
this.stack = this.stack.slice(0, this.currentIndex + 1);
this.stack.push({
schema: JSON.parse(JSON.stringify(schema)), // 深拷贝
timestamp: Date.now(),
description,
});
// 限制历史栈大小
if (this.stack.length > this.maxSize) {
this.stack.shift();
}
this.currentIndex = this.stack.length - 1;
}
undo(): any | null {
if (this.currentIndex > 0) {
this.currentIndex--;
return JSON.parse(JSON.stringify(this.stack[this.currentIndex].schema));
}
return null;
}
redo(): any | null {
if (this.currentIndex < this.stack.length - 1) {
this.currentIndex++;
return JSON.parse(JSON.stringify(this.stack[this.currentIndex].schema));
}
return null;
}
canUndo(): boolean {
return this.currentIndex > 0;
}
canRedo(): boolean {
return this.currentIndex < this.stack.length - 1;
}
clear(): void {
this.stack = [];
this.currentIndex = -1;
}
}
这个管理器实现了有限状态的深拷贝快照,核心要点是对 Schema 进行深拷贝,避免引用共享导致的历史篡改。
11.8 与前端框架生态集成
我们的渲染引擎基于 React,但低代码平台的应用场景远不止 React:
场景一:嵌入现有 React 应用
// 在 React 项目中使用
import { Renderer, registerDefaults } from 'lowcode-engine';
registerDefaults();
function MyPage() {
const schema = loadSchemaFromApi();
return <Renderer schema={schema} />;
}
场景二:独立嵌入到任何页面
<!-- 在普通 HTML 页面中通过 CDN 引入 -->
<div id="root"></div>
<script src="https://cdn.example.com/lowcode-engine.min.js"></script>
<script>
LowCodeEngine.mount('#root', {
schema: fetchedSchema,
dataSource: initialData,
});
</script>
场景三:iframe 安全模式
在大厂的安全实践中,往往将渲染引擎放在 iframe 中运行,与主应用隔离。这样 Schema 中的恶意代码不会影响主应用的 DOM 和数据:
<!-- 主应用 -->
<iframe
id="lowcode-iframe"
src="/renderer.html"
sandbox="allow-scripts allow-same-origin"
/>
// 跨域通信
const iframe = document.getElementById('lowcode-iframe');
iframe.contentWindow.postMessage({
type: 'RENDER_SCHEMA',
schema: currentSchema,
dataSource: pageData,
}, '*');
window.addEventListener('message', (event) => {
if (event.data.type === 'COMPONENT_EVENT') {
handleEvent(event.data.payload);
}
});
11.9 服务端渲染 (SSR) 支持
低代码生成的页面如果需要良好的 SEO 和首屏体验,就要支持 SSR:
// server-renderer.ts
import React from 'react';
import { renderToString } from 'react-dom/server';
import { Renderer } from './Renderer';
import { componentManager } from './registry';
async function renderLowCodePage(schema: any, dataSource: Record<string, any>): Promise<string> {
// 确保所有组件已注册(服务端注册)
ensureComponentsRegistered();
const html = renderToString(
React.createElement(Renderer, {
schema,
dataSource,
})
);
// 注入初始数据到 HTML 用于客户端 hydrate
const script = `
<script>
window.__INITIAL_DATA__ = ${JSON.stringify(dataSource)};
window.__SCHEMA__ = ${JSON.stringify(schema)};
</script>
`;
return `
<!DOCTYPE html>
<html>
<head>
<title>${schema.title || 'Low-Code Page'}</title>
</head>
<body>
<div id="root">${html}</div>
${script}
<script src="/lowcode-client.js"></script>
</body>
</html>
`;
}
SSR 实现中的关键注意点:
- 组件兼容性:服务端不能使用
useEffect、window、document等浏览器专用 API - 数据预取:在 SSR 前需要提前获取所有异步数据
- Hydrate 匹配:客户端首次渲染的 HTML 结构必须与 SSR 产物完全一致
十二、总结与展望
12.1 我们做了什么
在这篇文章中,我们从零起步,一步步构建了一个功能完整的低代码渲染引擎。回顾一下我们覆盖的内容:
| 模块 | 核心能力 | 关键设计 |
|---|---|---|
| 组件注册表 | 组件注册、查找、批量管理 | 单例模式、类型安全 |
| 渲染引擎 | Schema 解析、递归渲染、上下文传递 | RendererContext、React.memo |
| 表达式引擎 | 数据绑定、动态取值、沙箱执行 | new Function + Proxy 沙箱 |
| 条件渲染 | visible/disabled 条件、多运算符 | AND 逻辑求值器 |
| 事件系统 | 事件声明、动作链、动作分发 | Action 队列、异步调度 |
| 布局系统 | 24 栅格、Flexbox 布局 | Row/Col 设计 |
| 性能优化 | memo、Schema 缓存、懒加载、细粒度订阅 | LRU 缓存、按需加载 |
全部代码约 600 行(含类型定义),实现了一个可以渲染真实页面的低代码引擎核心。这就是很多商用低代码平台的最小可行性内核。
12.2 可以扩展的方向
一个完整的低代码平台远不止这些:
- 可视化设计器:拖拽生成 Schema 的编辑器(最直观的扩展方向)
- 版本管理:Schema 的版本控制、发布回滚
- 自定义组件 SDK:允许第三方开发者编写和上传自定义组件
- 跨端渲染:同一 Schema 渲染为 Web、移动端 H5、小程序
- AI 辅助生成:通过自然语言描述自动生成 Schema
- 公式引擎:更复杂的 Excel 风格公式(如 SUM、IF、VLOOKUP 等)
- 数据源管理:对接 REST API、GraphQL、数据库直连
- 权限控制:组件级别的可见性和可操作性权限
12.3 与主流低代码方案对比
理解了我们手写的引擎,再看市面上的主流方案,就豁然开朗了:
| 方案 | 核心技术 | Schema 格式 | 渲染方式 |
|---|---|---|---|
| 本文引擎 | React + TypeScript | 自定义 JSON | 递归渲染 + Context |
| 阿里 lowcode-engine | React + 物料协议 | 标准化物料描述 | WebComponent 隔离渲染 |
| 百度 amis | React + JSON | amis Schema | 自动解析 + 内置组件集 |
| 腾讯 hippo | Vue + JSON | Hippo Schema | 递归渲染 + 插件系统 |
| 微软 Power Apps | 闭源 PasS | Canvas App 公式 | 专利渲染器 |
核心洞察:
- 所有方案的底层逻辑完全一致——JSON Schema 驱动组件渲染
- 区别在于:Schema 协议的标准化程度、组件生态的丰富度、设计器的完善度
- 我们手写的引擎虽然只有数百行,但核心原理与大厂产品是相通的
这也说明了一个道理:不要被"低代码"这个词吓到。它的底层就是 DOM 树和配置对象的映射,先理解了渲染引擎,其他部分不过是锦上添花。
12.4 常见误区
在构建低代码引擎的过程中,有几点容易踩坑:
误区一:追求"万能 Schema"
初次设计 Schema 时容易想太多,试图用一个协议描述所有场景。结果协议越来越复杂,变成"JSON 版的编程语言"。正确的做法是:保持 Schema 简单,复杂性交给组件内部实现。
例如,不需要在 Schema 中定义数据转换逻辑(那是表达式引擎的事),不需要在 Schema 中定义组件内交互(那是组件自己的事)。Schema 只描述"用什么组件、是什么配置、关联什么事件"就够了。
误区二:忽略性能
一个包含 200 个组件的表单页面,如果每次数据变化都全量重渲染,很快就会卡顿。细粒度依赖追踪是生产级引擎的标配。用本文的 ReactiveStore 只更新实际变化的组件,或者用 React 的 useMemo + useCallback 精确控制渲染范围。
误区三:Schema 写死在代码里
很多初学者把 Schema 当作普通 JSON 定义在代码中,这完全丧失了低代码的灵活性。应该将 Schema 存储在后端数据库或配置中心,通过 API 实时加载。这样,运营人员通过设计器修改页面后,修改立即对所有用户生效——不需要发版、不需要审批 AppStore、不需要用户更新客户端。
误区四:忽略组件版本管理
当组件库升级时,存量页面可能使用了旧版本的组件。例如 Button 组件 v2 改了 API,但用户的 Schema 还是按 v1 的 props 配置的。解决方案:组件注册表需要支持多版本共存:
register({ type: 'Button', version: '1.0', component: ButtonV1 });
register({ type: 'Button', version: '2.0', component: ButtonV2 });
Schema 中指定版本:
{ "type": "Button@2.0", "props": { ... } }
这样,存量页面可以渐进式升级组件,而不是一把梭全部切换。
12.5 当我们谈论低代码时,我们在谈论什么
最后,我想分享一点个人思考。
低代码平台本质上是一个运行时 DSL——它定义了一种描述 UI 和交互的"语言",然后用渲染引擎来"解释执行"这种语言。
为什么用 JSON?因为它通用、跨语言、结构化。但 JSON 只是选择之一,你也可以用 YAML、XML 甚至 Protobuf。形式不重要,核心是"声明式 → 渲染"这条链路。
低代码的真正价值在于:
- 效率:写一次组件,复用千百次
- 一致性:所有页面共用同一套组件和设计语言
- 非技术人员赋能:运营、产品经理也可以搭建页面
- 灰度与试验:Schema 版本化,可以做 A/B 测试
但低代码不能解决所有问题。高复杂度的业务逻辑、自定义交互、性能敏感的场景,还是要回到传统开发模式。
我的建议是:用低代码处理 80% 的标准化场景,预留 20% 的"自定义组件"扩展点。这个 80/20 法则,是很多成功低代码平台的共同特征。
📌 DeepSeek 实战指南系列:如果你对 AI 驱动的低代码开发感兴趣,推荐阅读我的专栏系列文章——DeepSeek 实战指南,深入了解如何利用大模型加速低代码开发效率。
当然,我们的渲染引擎目前还缺少一些关键的生产级特性:
- 可视化设计器:用户需要在界面上拖拽配置,而不是手写 JSON
- 实时预览:配置即所得,拖拽组件后立刻看到效果
- 版本管理:发布、回滚、灰度发布等工程化能力
- 审批流程:Schema 变更需要走审批流
- 数据分析:页面埋点、数据采集、用户行为分析
不过,这些都属于"可叠加的增值功能"。渲染引擎是低代码平台的技术底座,底座稳了,楼上怎么建都行。希望本文能帮你从"知其然"到"知其所以然"。
最后:如果你正在考虑团队是否要引入低代码,我的建议是——先不要买平台,先写一个原型。花一周时间搭一个像本文这样的渲染引擎,然后在内部找出一个小功能(比如后台表单页)做试点。当你亲手走通了"Schema → 渲染 → 用户反馈"这个闭环之后,你才能真正理解低代码适合什么、不适合什么,以及需要投入多少资源。这是花钱买商业平台学不来的经验。
本文所有代码已开源,可直接复用用于学习和项目原型。如果你在实现过程中遇到问题,或对低代码架构有独特见解,欢迎在评论区留言讨论。
更多推荐

所有评论(0)