一、引言

"低代码"这个词汇在近两三年几乎成了软件开发领域最热门的标签之一。然而,在这股热潮背后,真正理解低代码平台底层原理的开发者却并不多。很多人用过低代码平台,但面对"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,渲染引擎需要完成以下任务:

  1. Schema 解析:将 JSON 解析为内存中的组件树
  2. 组件查找:根据 type 从组件注册表中找到对应的渲染组件
  3. 属性注入:将 props 传递给组件,处理默认值和类型转换
  4. 递归渲染:遍历 children,递归渲染子组件
  5. 上下文传递:管理数据上下文、事件上下文、样式上下文
  6. 生命周期管理:组件的挂载、更新、卸载

三、组件注册与管理

渲染引擎的第一步是管理可用的组件。我们需要一个全局的组件注册表,让引擎知道给定 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: {} } },
]);

这种注册机制有两大好处:

  1. 解耦:渲染引擎不依赖具体组件,只依赖注册表接口
  2. 可扩展:任何人都可以注册自定义组件,引擎无需修改

四、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>;
});

这段代码虽然只有几十行,但实现了渲染引擎最核心的五个能力

  1. 动态组件查找:根据 type 从注册表找组件
  2. 条件渲染:支持 visible/disabled 条件
  3. 属性合并:默认属性 + 用户配置属性
  4. 表达式解析{{xxx}} 数据绑定
  5. 递归渲染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 构造了一个安全的作用域:

  1. Proxy 拦截属性访问,确保只能访问 context 中的数据结构
  2. has trap 返回 true,避免 with 作用域中因为变量未定义而报错
  3. new Function 动态创建函数执行表达式,比 eval 更安全

虽然这种沙箱并不能完全防止恶意代码(new Function 仍然能访问原型链),但对于低代码平台中可信的 Schema 配置来说已经足够。生产环境可以使用更严格的沙箱方案,如 iframevm2

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_displaytax_info 都通过表达式引擎动态计算。当用户在 price_inputquantity_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}}"
                }
              }
            }
          }
        ]
      }
    }
  }
}

这个配置实现了完整的三级联动逻辑:

  1. 省份变化 → 清空城市和区县的值与选项
  2. 调用后端 API 获取该省份的城市列表
  3. 将 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 实现中的关键注意点:

  1. 组件兼容性:服务端不能使用 useEffectwindowdocument 等浏览器专用 API
  2. 数据预取:在 SSR 前需要提前获取所有异步数据
  3. 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 可以扩展的方向

一个完整的低代码平台远不止这些:

  1. 可视化设计器:拖拽生成 Schema 的编辑器(最直观的扩展方向)
  2. 版本管理:Schema 的版本控制、发布回滚
  3. 自定义组件 SDK:允许第三方开发者编写和上传自定义组件
  4. 跨端渲染:同一 Schema 渲染为 Web、移动端 H5、小程序
  5. AI 辅助生成:通过自然语言描述自动生成 Schema
  6. 公式引擎:更复杂的 Excel 风格公式(如 SUM、IF、VLOOKUP 等)
  7. 数据源管理:对接 REST API、GraphQL、数据库直连
  8. 权限控制:组件级别的可见性和可操作性权限

12.3 与主流低代码方案对比

理解了我们手写的引擎,再看市面上的主流方案,就豁然开朗了:

方案核心技术Schema 格式渲染方式
本文引擎React + TypeScript自定义 JSON递归渲染 + Context
阿里 lowcode-engineReact + 物料协议标准化物料描述WebComponent 隔离渲染
百度 amisReact + JSONamis Schema自动解析 + 内置组件集
腾讯 hippoVue + JSONHippo Schema递归渲染 + 插件系统
微软 Power Apps闭源 PasSCanvas App 公式专利渲染器

核心洞察

  1. 所有方案的底层逻辑完全一致——JSON Schema 驱动组件渲染
  2. 区别在于:Schema 协议的标准化程度、组件生态的丰富度、设计器的完善度
  3. 我们手写的引擎虽然只有数百行,但核心原理与大厂产品是相通的

这也说明了一个道理:不要被"低代码"这个词吓到。它的底层就是 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。形式不重要,核心是"声明式 → 渲染"这条链路

低代码的真正价值在于:

  1. 效率:写一次组件,复用千百次
  2. 一致性:所有页面共用同一套组件和设计语言
  3. 非技术人员赋能:运营、产品经理也可以搭建页面
  4. 灰度与试验:Schema 版本化,可以做 A/B 测试

但低代码不能解决所有问题。高复杂度的业务逻辑、自定义交互、性能敏感的场景,还是要回到传统开发模式。

我的建议是:用低代码处理 80% 的标准化场景,预留 20% 的"自定义组件"扩展点。这个 80/20 法则,是很多成功低代码平台的共同特征。


📌 DeepSeek 实战指南系列:如果你对 AI 驱动的低代码开发感兴趣,推荐阅读我的专栏系列文章——DeepSeek 实战指南,深入了解如何利用大模型加速低代码开发效率。

当然,我们的渲染引擎目前还缺少一些关键的生产级特性:
- 可视化设计器:用户需要在界面上拖拽配置,而不是手写 JSON
- 实时预览:配置即所得,拖拽组件后立刻看到效果
- 版本管理:发布、回滚、灰度发布等工程化能力
- 审批流程:Schema 变更需要走审批流
- 数据分析:页面埋点、数据采集、用户行为分析

不过,这些都属于"可叠加的增值功能"。渲染引擎是低代码平台的技术底座,底座稳了,楼上怎么建都行。希望本文能帮你从"知其然"到"知其所以然"。

最后:如果你正在考虑团队是否要引入低代码,我的建议是——先不要买平台,先写一个原型。花一周时间搭一个像本文这样的渲染引擎,然后在内部找出一个小功能(比如后台表单页)做试点。当你亲手走通了"Schema → 渲染 → 用户反馈"这个闭环之后,你才能真正理解低代码适合什么、不适合什么,以及需要投入多少资源。这是花钱买商业平台学不来的经验。

本文所有代码已开源,可直接复用用于学习和项目原型。如果你在实现过程中遇到问题,或对低代码架构有独特见解,欢迎在评论区留言讨论。

更多推荐