设计低代码/可视化搭建平台
需求分析
低代码平台(Low-Code Platform)是前端系统设计面试中的高频题目,它综合考察了 Schema 协议设计、拖拽引擎、渲染引擎、组件体系、数据流管理 等核心前端架构能力。低代码的核心理念是通过可视化拖拽 + 配置的方式,让非专业开发者也能快速搭建应用页面,同时保留专业开发者通过代码扩展的能力。
低代码平台的系统设计题考察的不是某个具体 API 的使用,而是架构分层能力、协议抽象能力和复杂交互实现能力。回答时应从需求分析入手,先定义清楚设计态和运行态的边界,再逐步展开各模块设计。
核心概念
在深入架构之前,先厘清低代码平台的三个核心概念:
| 概念 | 说明 | 类比 |
|---|---|---|
| 设计态(Design Time) | 用户在编辑器中拖拽、配置组件的阶段 | 类似 Photoshop 编辑文件 |
| 运行态(Runtime) | 用户发布后,最终用户访问页面的阶段 | 类似浏览器打开网页 |
| Schema 协议 | 设计态与运行态之间的桥梁,用 JSON 描述页面结构 | 类似 HTML 描述 DOM 结构 |
设计态和运行态的分离是低代码平台最关键的架构决策。设计态需要额外的编辑能力(选中、拖拽、配置面板),运行态则追求轻量和高性能。两者共享同一套 Schema 协议,但渲染器不同。
功能需求
| 功能模块 | 核心能力 | 说明 |
|---|---|---|
| 可视化编辑器 | 拖拽搭建、组件配置、实时预览 | 所见即所得的搭建体验 |
| 组件市场 | 基础组件、业务组件、容器组件 | 丰富的物料生态 |
| 属性面板 | 样式配置、事件绑定、数据绑定 | 可视化的组件属性编辑 |
| 数据源管理 | API 绑定、变量管理、表达式引擎 | 动态数据驱动页面 |
| 预览与发布 | Schema 渲染、源码出码、版本管理 | 从设计到上线的完整链路 |
| 撤销重做 | 操作历史、快照恢复 | 完善的编辑体验 |
非功能需求
面试中不仅要设计功能模块,还要关注非功能需求,这是区分初级和高级工程师的关键。
- 编辑流畅性:拖拽操作 60fps,属性变更实时反映到画布
- Schema 可扩展:支持自定义组件、自定义属性面板
- 产物性能:运行态页面首屏 < 1s,支持按需加载组件
- 跨框架:渲染引擎支持 React/Vue 等多框架
- 协同编辑:多人同时编辑同一页面,实时同步(进阶)
整体架构
分层架构
模块职责
| 模块 | 职责 | 关键技术 |
|---|---|---|
| 编辑器 Editor | 画布渲染、组件选中、拖拽放置 | iframe 隔离、postMessage |
| 拖拽引擎 | 拖入、拖拽排序、碰撞检测、辅助线 | HTML5 DnD / Pointer Events |
| 属性面板 | 组件属性编辑、样式配置、事件绑定 | JSON Schema 驱动表单 |
| Schema 管理 | 维护页面 Schema 树、CRUD 操作 | Immer 不可变数据 |
| 历史管理 | 撤销/重做、操作快照 | Command 命令模式 |
| 渲染引擎 | Schema 转 VNode、组件实例化 | 递归渲染、动态组件 |
| 表达式引擎 | 解析变量绑定、计算表达式 | 安全沙箱执行 |
| 出码引擎 | Schema 转源代码 | AST 生成、代码模板 |
核心模块设计
1. Schema 协议设计
Schema 是低代码平台的灵魂,它定义了页面的完整描述格式。一个好的 Schema 协议需要做到:描述能力完备、可扩展、序列化友好。
/** 组件节点 Schema */
interface ComponentSchema {
/** 唯一标识 */
id: string;
/** 组件类型,与组件注册表对应 */
componentName: string;
/** 组件属性 */
props: Record<string, PropValue>;
/** 子节点(容器组件) */
children?: ComponentSchema[];
/** 样式配置 */
style?: Record<string, string | number>;
/** 事件绑定 */
events?: Record<string, EventHandler>;
/** 条件渲染 */
condition?: Expression | boolean;
/** 循环渲染 */
loop?: {
data: Expression;
itemName: string;
indexName?: string;
};
}
/** 属性值支持静态值和表达式 */
type PropValue =
| string
| number
| boolean
| null
| Expression
| PropValue[]
| { [key: string]: PropValue };
/** 表达式类型 - 用于动态绑定 */
interface Expression {
type: 'expression';
/** 表达式内容,如 ``state.count + 1`` */
value: string;
}
/** 事件处理器 */
interface EventHandler {
type: 'action';
/** 动作类型 */
actionType: 'setState' | 'fetch' | 'navigate' | 'custom';
/** 动作参数 */
params: Record<string, PropValue>;
}
页面级 Schema 除了组件树之外,还需要包含数据源、全局状态等信息:
/** 页面 Schema */
interface PageSchema {
/** Schema 版本号 */
version: string;
/** 页面元信息 */
meta: {
title: string;
description?: string;
};
/** 组件树 - 页面的核心描述 */
componentTree: ComponentSchema;
/** 数据源配置 */
dataSources: DataSourceConfig[];
/** 全局状态 */
state: Record<string, PropValue>;
/** 生命周期钩子 */
lifeCycles?: {
onMount?: EventHandler;
onUnmount?: EventHandler;
};
}
/** 数据源配置 */
interface DataSourceConfig {
id: string;
type: 'fetch' | 'websocket' | 'static';
options: FetchOptions | WebSocketOptions | StaticOptions;
/** 自动请求配置 */
autoFetch?: boolean;
/** 依赖的状态变量,变化时自动重新请求 */
deps?: string[];
}
interface FetchOptions {
url: string | Expression;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: Record<string, string | Expression>;
body?: Record<string, PropValue>;
/** 数据转换 */
transformer?: Expression;
}
一个实际的 Schema 示例:
点击展开完整 Schema 示例
{
"version": "1.0.0",
"meta": { "title": "用户管理" },
"state": {
"searchText": "",
"currentPage": 1
},
"dataSources": [
{
"id": "userList",
"type": "fetch",
"options": {
"url": "/api/users",
"method": "GET"
},
"autoFetch": true,
"deps": ["searchText", "currentPage"]
}
],
"componentTree": {
"id": "root",
"componentName": "Page",
"props": {},
"children": [
{
"id": "search",
"componentName": "Input",
"props": {
"placeholder": "搜索用户",
"value": { "type": "expression", "value": "state.searchText" }
},
"events": {
"onChange": {
"type": "action",
"actionType": "setState",
"params": {
"key": "searchText",
"value": { "type": "expression", "value": "event.target.value" }
}
}
}
},
{
"id": "table",
"componentName": "Table",
"props": {
"dataSource": { "type": "expression", "value": "dataSource.userList.data" },
"loading": { "type": "expression", "value": "dataSource.userList.loading" }
}
}
]
}
}
2. 拖拽引擎
拖拽引擎是编辑器交互的核心,需要支持从组件面板拖入画布、画布内拖拽排序、跨容器拖拽等操作。
/** 拖拽引擎 */
class DndEngine {
private dragState: DragState | null = null;
private guidelines: Guideline[] = [];
/** 开始拖拽 */
startDrag(source: DragSource): void {
this.dragState = {
source,
position: { x: 0, y: 0 },
dropTarget: null,
insertIndex: -1,
};
this.emit('dragStart', this.dragState);
}
/** 拖拽过程中 - 核心逻辑 */
onDragMove(event: PointerEvent): void {
if (!this.dragState) return;
const { clientX, clientY } = event;
this.dragState.position = { x: clientX, y: clientY };
// 1. 碰撞检测:找到当前悬停的容器
const dropTarget = this.detectCollision(clientX, clientY);
// 2. 计算插入位置
const insertIndex = dropTarget
? this.calculateInsertIndex(dropTarget, clientY)
: -1;
// 3. 计算辅助线(吸附参考线)
this.guidelines = this.calculateGuidelines(clientX, clientY);
this.dragState.dropTarget = dropTarget;
this.dragState.insertIndex = insertIndex;
this.emit('dragMove', {
...this.dragState,
guidelines: this.guidelines,
});
}
/** 碰撞检测 - 判断拖拽元素落在哪个容器上 */
private detectCollision(x: number, y: number): ContainerNode | null {
// 从组件树的叶子节点向上查找,优先匹配最内层容器
const containers = this.getContainerNodes();
// 按层级从深到浅排序,优先匹配最内层
containers.sort((a, b) => b.depth - a.depth);
for (const container of containers) {
const rect = container.getBoundingRect();
if (
x >= rect.left &&
x <= rect.right &&
y >= rect.top &&
y <= rect.bottom
) {
return container;
}
}
return null;
}
/** 计算辅助线和吸附 */
private calculateGuidelines(x: number, y: number): Guideline[] {
const SNAP_THRESHOLD = 5; // 吸附阈值 5px
const lines: Guideline[] = [];
const siblings = this.getSiblingRects();
for (const sibling of siblings) {
// 左对齐
if (Math.abs(x - sibling.left) < SNAP_THRESHOLD) {
lines.push({ type: 'vertical', position: sibling.left });
}
// 右对齐
if (Math.abs(x - sibling.right) < SNAP_THRESHOLD) {
lines.push({ type: 'vertical', position: sibling.right });
}
// 上对齐
if (Math.abs(y - sibling.top) < SNAP_THRESHOLD) {
lines.push({ type: 'horizontal', position: sibling.top });
}
// 水平居中对齐
const centerX = (sibling.left + sibling.right) / 2;
if (Math.abs(x - centerX) < SNAP_THRESHOLD) {
lines.push({ type: 'vertical', position: centerX });
}
}
return lines;
}
/** 结束拖拽 - 执行放置操作 */
endDrag(): void {
if (!this.dragState?.dropTarget) return;
const { source, dropTarget, insertIndex } = this.dragState;
if (source.type === 'new') {
// 从组件面板拖入:创建新节点
this.schemaStore.insertNode(
dropTarget.id,
createComponentSchema(source.componentName),
insertIndex
);
} else {
// 画布内拖拽:移动节点
this.schemaStore.moveNode(
source.nodeId,
dropTarget.id,
insertIndex
);
}
this.dragState = null;
this.emit('dragEnd');
}
}
interface DragState {
source: DragSource;
position: { x: number; y: number };
dropTarget: ContainerNode | null;
insertIndex: number;
}
type DragSource =
| { type: 'new'; componentName: string }
| { type: 'move'; nodeId: string };
interface Guideline {
type: 'horizontal' | 'vertical';
position: number;
}
3. 画布设计
低代码平台通常支持多种布局模式。不同的业务场景选择不同的布局策略:
- 自由布局
- 流式布局
- 栅格布局
特点:组件可以放置在画布任意位置,支持绝对定位、旋转、缩放。
适用场景:营销海报、H5 活动页、数据大屏。
/** 自由布局画布 */
class FreeLayoutCanvas {
/** 将组件定位信息写入 style */
getComponentStyle(node: ComponentSchema): Record<string, string> {
return {
position: 'absolute',
left: `${node.props.x ?? 0}px`,
top: `${node.props.y ?? 0}px`,
width: `${node.props.width ?? 100}px`,
height: `${node.props.height ?? 40}px`,
transform: `rotate(${node.props.rotate ?? 0}deg)`,
};
}
/** 拖拽结束时计算落点坐标 */
calculateDropPosition(
event: PointerEvent,
canvasRect: DOMRect,
zoom: number
): { x: number; y: number } {
return {
x: (event.clientX - canvasRect.left) / zoom,
y: (event.clientY - canvasRect.top) / zoom,
};
}
}
特点:组件按文档流排列,支持 Flex 布局配置。
适用场景:表单页面、管理后台、文章详情页。
/** 流式布局画布 */
class FlowLayoutCanvas {
/** 容器组件的布局配置 */
getContainerStyle(node: ComponentSchema): Record<string, string> {
const layout = node.props.layout ?? {};
return {
display: 'flex',
flexDirection: layout.direction ?? 'column',
justifyContent: layout.justifyContent ?? 'flex-start',
alignItems: layout.alignItems ?? 'stretch',
gap: `${layout.gap ?? 8}px`,
padding: `${layout.padding ?? 16}px`,
};
}
/** 计算拖拽插入位置 - 基于子元素的中线 */
calculateInsertIndex(
container: HTMLElement,
mouseY: number
): number {
const children = Array.from(container.children);
for (let i = 0; i < children.length; i++) {
const rect = children[i].getBoundingClientRect();
const midY = rect.top + rect.height / 2;
if (mouseY < midY) return i;
}
return children.length;
}
}
特点:基于 12/24 栅格系统,支持响应式断点配置。
适用场景:仪表盘、数据看板、响应式页面。
/** 栅格布局配置 */
interface GridConfig {
/** 栅格列数 */
columns: 12 | 24;
/** 列间距 */
gutter: number;
/** 响应式断点 */
breakpoints?: {
xs?: number; // < 576px
sm?: number; // >= 576px
md?: number; // >= 768px
lg?: number; // >= 992px
xl?: number; // >= 1200px
};
}
/** 栅格组件 Schema 扩展 */
interface GridComponentSchema extends ComponentSchema {
props: ComponentSchema['props'] & {
/** 占据的栅格数 */
span: number | Record<string, number>;
/** 偏移栅格数 */
offset?: number;
};
}
/** 栅格布局画布 */
class GridLayoutCanvas {
constructor(private config: GridConfig) {}
getGridStyle(span: number): Record<string, string> {
const width = (span / this.config.columns) * 100;
return {
width: `${width}%`,
paddingLeft: `${this.config.gutter / 2}px`,
paddingRight: `${this.config.gutter / 2}px`,
};
}
}
4. 组件体系
组件是低代码平台的基础积木。完善的组件体系需要定义统一的物料协议(Material Protocol),确保第三方组件也能接入平台。
/** 物料描述协议 */
interface MaterialDescription {
/** 组件名称 */
componentName: string;
/** 显示名称 */
title: string;
/** 组件分类 */
category: 'basic' | 'layout' | 'business' | 'chart';
/** 组件图标 */
icon: string;
/** 组件描述 */
description: string;
/** 属性描述 - 用于生成属性面板 */
props: PropDescription[];
/** 是否为容器组件 */
isContainer: boolean;
/** 可接受的子组件类型(容器组件) */
allowChildren?: string[];
/** 组件默认配置 */
defaultProps: Record<string, unknown>;
/** 组件代码片段 - 用于拖入时创建初始 Schema */
snippets: ComponentSchema[];
}
/** 属性描述 */
interface PropDescription {
name: string;
title: string;
/** 属性编辑器类型 */
setter: SetterType;
/** 默认值 */
defaultValue?: unknown;
/** 是否支持表达式绑定 */
supportExpression?: boolean;
/** 属性分组 */
group?: 'basic' | 'style' | 'advanced';
}
type SetterType =
| 'StringSetter'
| 'NumberSetter'
| 'BoolSetter'
| 'SelectSetter'
| 'ColorSetter'
| 'JsonSetter'
| 'EventSetter'
| 'ExpressionSetter'
| { componentName: string; props: Record<string, unknown> };
组件分类与典型示例:
| 分类 | 说明 | 典型组件 |
|---|---|---|
| 基础组件 | 通用 UI 组件 | Button、Input、Select、Table、Form |
| 布局组件 | 容器型组件 | Row/Col、Tabs、Card、Modal、Drawer |
| 业务组件 | 特定业务场景 | 用户选择器、地址选择、富文本编辑器 |
| 图表组件 | 数据可视化 | LineChart、BarChart、PieChart |
/** 组件注册表 - 管理所有可用组件 */
class ComponentRegistry {
private components = new Map<string, RegisteredComponent>();
/** 注册组件 */
register(
name: string,
component: React.ComponentType<unknown>,
material: MaterialDescription
): void {
this.components.set(name, { component, material });
}
/** 批量注册 */
registerAll(
entries: Array<{
name: string;
component: React.ComponentType<unknown>;
material: MaterialDescription;
}>
): void {
entries.forEach(({ name, component, material }) => {
this.register(name, component, material);
});
}
/** 获取组件 */
getComponent(name: string): React.ComponentType<unknown> | undefined {
return this.components.get(name)?.component;
}
/** 获取物料描述 */
getMaterial(name: string): MaterialDescription | undefined {
return this.components.get(name)?.material;
}
/** 按分类获取组件列表(用于组件面板展示) */
getByCategory(category: string): MaterialDescription[] {
const result: MaterialDescription[] = [];
this.components.forEach(({ material }) => {
if (material.category === category) {
result.push(material);
}
});
return result;
}
}
interface RegisteredComponent {
component: React.ComponentType<unknown>;
material: MaterialDescription;
}
5. 属性面板
属性面板根据组件的 PropDescription 动态生成表单,实现可视化的属性编辑。核心思路是 Setter 机制:每种属性类型对应一个 Setter 编辑器组件。
/** 属性面板核心逻辑 */
class PropertyPanel {
/** 根据物料描述生成属性面板配置 */
generatePanelConfig(
material: MaterialDescription,
currentProps: Record<string, PropValue>
): PanelConfig {
// 按分组组织属性
const groups: Record<string, PanelField[]> = {
basic: [],
style: [],
advanced: [],
};
for (const prop of material.props) {
const group = prop.group ?? 'basic';
groups[group].push({
name: prop.name,
title: prop.title,
setter: prop.setter,
value: currentProps[prop.name],
supportExpression: prop.supportExpression ?? false,
});
}
return { groups };
}
/** 属性变更处理 */
handlePropChange(
nodeId: string,
propName: string,
value: PropValue
): void {
// 通过 SchemaStore 更新节点属性
this.schemaStore.updateNodeProp(nodeId, propName, value);
}
}
interface PanelConfig {
groups: Record<string, PanelField[]>;
}
interface PanelField {
name: string;
title: string;
setter: SetterType;
value: PropValue;
supportExpression: boolean;
}
6. 数据源管理
数据源管理负责接口请求、全局状态和表达式计算的统一管理,是让页面"活起来"的关键。
/** 数据源管理器 */
class DataSourceManager {
private dataSources = new Map<string, DataSourceInstance>();
private state: Record<string, unknown> = {};
private expressionEngine: ExpressionEngine;
constructor(
private schema: PageSchema,
expressionEngine: ExpressionEngine
) {
this.expressionEngine = expressionEngine;
// 初始化全局状态
this.state = { ...schema.state };
// 初始化数据源
schema.dataSources.forEach((config) => {
this.dataSources.set(config.id, this.createInstance(config));
});
}
/** 创建数据源实例 */
private createInstance(config: DataSourceConfig): DataSourceInstance {
return {
config,
data: null,
loading: false,
error: null,
};
}
/** 执行数据源请求 */
async fetchDataSource(id: string): Promise<void> {
const instance = this.dataSources.get(id);
if (!instance || instance.config.type !== 'fetch') return;
const options = instance.config.options as FetchOptions;
instance.loading = true;
this.notifyChange();
try {
// 解析 URL 中的表达式
const url = this.resolveExpression(options.url);
const response = await fetch(url as string, {
method: options.method,
headers: this.resolveHeaders(options.headers),
body: options.body ? JSON.stringify(
this.resolveExpressionDeep(options.body)
) : undefined,
});
let data = await response.json();
// 数据转换
if (options.transformer) {
data = this.expressionEngine.evaluate(
options.transformer.value,
{ data, state: this.state }
);
}
instance.data = data;
instance.error = null;
} catch (error) {
instance.error = error as Error;
} finally {
instance.loading = false;
this.notifyChange();
}
}
/** 更新全局状态 */
setState(key: string, value: unknown): void {
this.state[key] = value;
// 检查依赖该状态的数据源,自动重新请求
this.dataSources.forEach((instance, id) => {
if (instance.config.deps?.includes(key)) {
this.fetchDataSource(id);
}
});
this.notifyChange();
}
/** 获取当前上下文(供表达式引擎使用) */
getContext(): ExpressionContext {
const dataSourceContext: Record<string, unknown> = {};
this.dataSources.forEach((instance, id) => {
dataSourceContext[id] = {
data: instance.data,
loading: instance.loading,
error: instance.error,
};
});
return {
state: this.state,
dataSource: dataSourceContext,
};
}
}
interface DataSourceInstance {
config: DataSourceConfig;
data: unknown;
loading: boolean;
error: Error | null;
}
interface ExpressionContext {
state: Record<string, unknown>;
dataSource: Record<string, unknown>;
}
表达式引擎是数据绑定的核心,它负责安全地执行用户编写的表达式:
/** 表达式引擎 - 安全沙箱执行 */
class ExpressionEngine {
/** 执行表达式 */
evaluate(expression: string, context: ExpressionContext): unknown {
// 使用 new Function 创建沙箱
// 只暴露安全的上下文变量,不暴露 window/document
const keys = Object.keys(context);
const values = Object.values(context);
try {
const fn = new Function(...keys, `return (${expression})`);
return fn(...values);
} catch (error) {
console.warn(`Expression error: ${expression}`, error);
return undefined;
}
}
/** 解析属性值中的表达式 */
resolveValue(value: PropValue, context: ExpressionContext): unknown {
if (this.isExpression(value)) {
return this.evaluate(value.value, context);
}
if (Array.isArray(value)) {
return value.map((item) => this.resolveValue(item, context));
}
if (typeof value === 'object' && value !== null) {
const resolved: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value)) {
resolved[k] = this.resolveValue(v as PropValue, context);
}
return resolved;
}
return value;
}
private isExpression(value: unknown): value is Expression {
return (
typeof value === 'object' &&
value !== null &&
(value as Expression).type === 'expression'
);
}
}
生产环境建议使用更安全的沙箱方案(如 quickjs-emscripten 或 ShadowRealm),避免 new Function 的安全风险。也可以限制表达式白名单语法,只允许属性访问、算术运算、三元表达式等安全操作。
7. 渲染引擎(Schema 转 VNode)
渲染引擎是连接 Schema 和真实 UI 的桥梁,核心职责是递归遍历 Schema 树,将每个节点转化为对应的 React/Vue 组件。
import React from 'react';
/** 渲染引擎 - Schema 转 React 组件 */
class SchemaRenderer {
constructor(
private registry: ComponentRegistry,
private dataSourceManager: DataSourceManager,
private expressionEngine: ExpressionEngine
) {}
/** 渲染入口 */
render(schema: ComponentSchema): React.ReactNode {
return this.renderNode(schema);
}
/** 递归渲染节点 */
private renderNode(node: ComponentSchema): React.ReactNode {
const context = this.dataSourceManager.getContext();
// 1. 条件渲染
if (node.condition !== undefined) {
const show = typeof node.condition === 'boolean'
? node.condition
: this.expressionEngine.evaluate(
(node.condition as Expression).value,
context
);
if (!show) return null;
}
// 2. 循环渲染
if (node.loop) {
return this.renderLoop(node, context);
}
// 3. 获取组件
const Component = this.registry.getComponent(node.componentName);
if (!Component) {
console.warn(`Component not found: ${node.componentName}`);
return null;
}
// 4. 解析属性中的表达式
const resolvedProps = this.resolveProps(node.props, context);
// 5. 解析事件
const eventHandlers = this.resolveEvents(node.events, context);
// 6. 递归渲染子节点
const children = node.children?.map((child) =>
this.renderNode(child)
);
return React.createElement(
Component,
{
key: node.id,
...resolvedProps,
...eventHandlers,
style: node.style,
},
children
);
}
/** 循环渲染 */
private renderLoop(
node: ComponentSchema,
context: ExpressionContext
): React.ReactNode {
const loopData = this.expressionEngine.evaluate(
(node.loop!.data as Expression).value,
context
) as unknown[];
if (!Array.isArray(loopData)) return null;
return loopData.map((item, index) => {
// 为每次循环创建独立的上下文
const loopContext: ExpressionContext = {
...context,
state: {
...context.state,
[node.loop!.itemName]: item,
[node.loop!.indexName ?? 'index']: index,
},
};
// 去掉 loop 属性,避免无限递归
const nodeWithoutLoop = { ...node, loop: undefined };
return this.renderNodeWithContext(
nodeWithoutLoop,
loopContext,
`${node.id}-${index}`
);
});
}
/** 解析事件处理器 */
private resolveEvents(
events: Record<string, EventHandler> | undefined,
context: ExpressionContext
): Record<string, (...args: unknown[]) => void> {
if (!events) return {};
const handlers: Record<string, (...args: unknown[]) => void> = {};
for (const [eventName, handler] of Object.entries(events)) {
handlers[eventName] = (...args: unknown[]) => {
this.executeAction(handler, { ...context, event: args[0] });
};
}
return handlers;
}
/** 执行动作 */
private executeAction(
handler: EventHandler,
context: ExpressionContext & { event?: unknown }
): void {
switch (handler.actionType) {
case 'setState': {
const key = handler.params.key as string;
const value = this.expressionEngine.resolveValue(
handler.params.value,
context
);
this.dataSourceManager.setState(key, value);
break;
}
case 'fetch': {
const dsId = handler.params.dataSourceId as string;
this.dataSourceManager.fetchDataSource(dsId);
break;
}
case 'navigate': {
const url = this.expressionEngine.resolveValue(
handler.params.url,
context
) as string;
window.location.href = url;
break;
}
}
}
}
8. 撤销重做
撤销重做基于 Command 命令模式 实现,每次操作记录一条命令,支持 undo/redo。
- 命令模式
- 具体命令实现
/** 命令接口 */
interface Command {
/** 执行操作 */
execute(): void;
/** 撤销操作 */
undo(): void;
/** 命令描述(用于操作历史面板) */
description: string;
}
/** 历史管理器 */
class HistoryManager {
private undoStack: Command[] = [];
private redoStack: Command[] = [];
private maxHistory = 50;
/** 执行命令并入栈 */
execute(command: Command): void {
command.execute();
this.undoStack.push(command);
// 执行新操作时清空重做栈
this.redoStack = [];
// 限制历史记录数量
if (this.undoStack.length > this.maxHistory) {
this.undoStack.shift();
}
}
/** 撤销 */
undo(): void {
const command = this.undoStack.pop();
if (!command) return;
command.undo();
this.redoStack.push(command);
}
/** 重做 */
redo(): void {
const command = this.redoStack.pop();
if (!command) return;
command.execute();
this.undoStack.push(command);
}
/** 是否可以撤销 */
get canUndo(): boolean {
return this.undoStack.length > 0;
}
/** 是否可以重做 */
get canRedo(): boolean {
return this.redoStack.length > 0;
}
}
/** 添加节点命令 */
class AddNodeCommand implements Command {
description: string;
constructor(
private schemaStore: SchemaStore,
private parentId: string,
private node: ComponentSchema,
private index: number
) {
this.description = `添加 ${node.componentName}`;
}
execute(): void {
this.schemaStore.insertNode(this.parentId, this.node, this.index);
}
undo(): void {
this.schemaStore.removeNode(this.node.id);
}
}
/** 更新属性命令 */
class UpdatePropCommand implements Command {
description: string;
private oldValue: PropValue;
constructor(
private schemaStore: SchemaStore,
private nodeId: string,
private propName: string,
private newValue: PropValue
) {
this.description = `修改 ${propName}`;
this.oldValue = this.schemaStore.getNodeProp(nodeId, propName);
}
execute(): void {
this.schemaStore.updateNodeProp(
this.nodeId,
this.propName,
this.newValue
);
}
undo(): void {
this.schemaStore.updateNodeProp(
this.nodeId,
this.propName,
this.oldValue
);
}
}
/** 移动节点命令 */
class MoveNodeCommand implements Command {
description = '移动组件';
private originalParentId: string;
private originalIndex: number;
constructor(
private schemaStore: SchemaStore,
private nodeId: string,
private newParentId: string,
private newIndex: number
) {
const location = this.schemaStore.getNodeLocation(nodeId);
this.originalParentId = location.parentId;
this.originalIndex = location.index;
}
execute(): void {
this.schemaStore.moveNode(
this.nodeId,
this.newParentId,
this.newIndex
);
}
undo(): void {
this.schemaStore.moveNode(
this.nodeId,
this.originalParentId,
this.originalIndex
);
}
}
9. 预览与发布
低代码平台的产物有两种主要形态:Schema 直接渲染 和 出码生成源代码。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Schema 渲染 | 实时更新、无需构建 | 运行时依赖渲染引擎、性能有上限 | 营销页面、后台表单 |
| 出码 | 性能好、可二次开发 | 需要构建部署、修改不便 | 正式项目、需要定制 |
/** 出码引擎 - Schema 转 React 源代码 */
class CodeGenerator {
/** 生成页面代码 */
generate(schema: PageSchema): GeneratedCode {
const imports = new Set<string>();
const componentCode = this.generateComponent(
schema.componentTree,
imports
);
const code = [
"import React, { useState, useEffect } from 'react';",
Array.from(imports).join('\n'),
'',
'export default function Page() {',
' // 状态',
this.generateState(schema.state),
'',
' // 数据源',
this.generateDataSources(schema.dataSources),
'',
' // 生命周期',
this.generateLifeCycles(schema.lifeCycles),
'',
' return (',
componentCode,
' );',
'}',
].join('\n');
return {
code,
fileName: 'Page.tsx',
};
}
/** 递归生成组件 JSX */
private generateComponent(
node: ComponentSchema,
imports: Set<string>
): string {
imports.add(
`import { ${node.componentName} } from '@/components';`
);
const propsStr = this.generateProps(node.props);
const eventsStr = this.generateEvents(node.events);
if (!node.children?.length) {
return ` <${node.componentName}${propsStr}${eventsStr} />`;
}
const childrenStr = node.children
.map((child) => this.generateComponent(child, imports))
.join('\n');
return [
` <${node.componentName}${propsStr}${eventsStr}>`,
childrenStr,
` </${node.componentName}>`,
].join('\n');
}
private generateState(
state: Record<string, PropValue>
): string {
return Object.entries(state)
.map(([key, value]) => {
const jsonValue = JSON.stringify(value);
return ` const [${key}, set${this.capitalize(key)}] = useState(${jsonValue});`;
})
.join('\n');
}
private capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
}
interface GeneratedCode {
code: string;
fileName: string;
}
10. 版本管理
版本管理确保页面可以安全回退、多人协作不冲突:
/** 版本管理器 */
class VersionManager {
/** 保存新版本 */
async saveVersion(
pageId: string,
schema: PageSchema,
message: string
): Promise<VersionRecord> {
const version: VersionRecord = {
id: generateId(),
pageId,
schema: JSON.parse(JSON.stringify(schema)),
message,
createdAt: new Date().toISOString(),
createdBy: getCurrentUser(),
};
await this.api.post('/versions', version);
return version;
}
/** 获取版本历史 */
async getHistory(pageId: string): Promise<VersionRecord[]> {
return this.api.get(
`/versions?pageId=${pageId}&sort=createdAt:desc`
);
}
/** 回退到指定版本 */
async rollback(versionId: string): Promise<PageSchema> {
const version = await this.api.get(`/versions/${versionId}`);
return version.schema;
}
/** 版本对比 - 计算两个 Schema 之间的差异 */
diff(
oldSchema: PageSchema,
newSchema: PageSchema
): SchemaDiff[] {
// 深度对比组件树,生成差异列表
return deepDiff(oldSchema.componentTree, newSchema.componentTree);
}
}
interface VersionRecord {
id: string;
pageId: string;
schema: PageSchema;
message: string;
createdAt: string;
createdBy: string;
}
性能优化
编辑态性能
| 优化策略 | 说明 | 效果 |
|---|---|---|
| iframe 隔离画布 | 编辑器 UI 和画布在不同 iframe 中,样式和脚本互不干扰 | 避免样式冲突、提升安全性 |
| 虚拟渲染 | 大纲树、组件列表使用虚拟滚动 | 支持 1000+ 组件不卡顿 |
| Schema diff 更新 | 属性变更只更新对应节点,不全量重渲染 | 响应速度 < 16ms |
| 拖拽节流 | dragMove 事件使用 requestAnimationFrame 节流 | 拖拽保持 60fps |
| Web Worker | 辅助线计算、Schema 校验等耗时操作放 Worker 中 | 不阻塞主线程 |
运行态性能
/** 组件懒加载 - 运行态按需加载组件代码 */
class LazyComponentRegistry {
private loaders = new Map<string, () => Promise<React.ComponentType<unknown>>>();
private cache = new Map<string, React.ComponentType<unknown>>();
/** 注册懒加载组件 */
registerLazy(
name: string,
loader: () => Promise<{ default: React.ComponentType<unknown> }>
): void {
this.loaders.set(name, async () => {
const module = await loader();
return module.default;
});
}
/** 获取组件(自动懒加载) */
async getComponent(
name: string
): Promise<React.ComponentType<unknown> | undefined> {
// 优先从缓存取
if (this.cache.has(name)) {
return this.cache.get(name);
}
const loader = this.loaders.get(name);
if (!loader) return undefined;
const component = await loader();
this.cache.set(name, component);
return component;
}
}
// 使用示例
const registry = new LazyComponentRegistry();
registry.registerLazy('Table', () => import('./components/Table'));
registry.registerLazy('Chart', () => import('./components/Chart'));
registry.registerLazy('RichText', () => import('./components/RichText'));
扩展设计
插件体系
低代码平台的可扩展性至关重要,通过插件机制实现功能扩展(参考前端 SDK 通用架构设计):
/** 插件接口 */
interface LowCodePlugin {
name: string;
/** 插件初始化 */
init(context: PluginContext): void;
/** 插件销毁 */
destroy?(): void;
}
/** 插件上下文 */
interface PluginContext {
/** 注册自定义组件 */
registerComponent: ComponentRegistry['register'];
/** 注册自定义 Setter */
registerSetter: (name: string, component: React.ComponentType) => void;
/** 注册自定义动作 */
registerAction: (name: string, handler: ActionHandler) => void;
/** 监听编辑器事件 */
on: (event: string, handler: (...args: unknown[]) => void) => void;
/** Schema 操作 */
schema: {
get: () => PageSchema;
update: (updater: (draft: PageSchema) => void) => void;
};
}
/** 插件管理器 */
class PluginManager {
private plugins: LowCodePlugin[] = [];
async loadPlugin(plugin: LowCodePlugin): Promise<void> {
const context = this.createContext();
plugin.init(context);
this.plugins.push(plugin);
}
destroyAll(): void {
this.plugins.forEach((p) => p.destroy?.());
this.plugins = [];
}
}
多框架渲染
通过抽象渲染层,支持不同前端框架:
常见面试问题
Q1: 低代码平台的 Schema 应该如何设计?需要考虑哪些关键点?
答案:
Schema 是低代码平台的核心数据结构,设计时需要从以下几个维度考虑:
1. 组件描述的完备性
Schema 需要能够完整描述一个组件的所有信息:
// 一个完备的组件 Schema 至少包含以下字段
interface ComponentSchema {
id: string; // 唯一标识
componentName: string; // 组件类型
props: Record<string, PropValue>; // 属性
children?: ComponentSchema[]; // 子节点
style?: Record<string, string | number>; // 样式
events?: Record<string, EventHandler>; // 事件
condition?: Expression | boolean; // 条件渲染
loop?: LoopConfig; // 循环渲染
}
2. 静态值与动态表达式的区分
属性值既要支持静态值(硬编码),也要支持动态表达式(绑定变量)。通过 type: 'expression' 标记来区分:
{
"props": {
"title": "静态标题",
"visible": { "type": "expression", "value": "state.showDialog" },
"data": { "type": "expression", "value": "dataSource.userList.data" }
}
}
3. 关键设计原则
| 原则 | 说明 |
|---|---|
| JSON 序列化友好 | 所有值都能 JSON.stringify,方便存储和传输 |
| 版本化 | Schema 带 version 字段,支持协议升级和迁移 |
| 可扩展 | 预留 extraProps 等扩展字段,不破坏现有结构 |
| 跨框架 | 不绑定具体框架概念,React/Vue 都能渲染 |
| 人类可读 | 字段命名清晰,开发者可以直接编辑 JSON |
Q2: 拖拽引擎的核心实现原理是什么?如何处理嵌套容器的拖拽?
答案:
拖拽引擎的核心流程分为三个阶段:dragStart -> dragMove -> dragEnd。
1. 碰撞检测算法
拖拽过程中的关键是判断当前拖拽元素落在哪个容器上。对于嵌套容器,需要从内到外逐层检测,优先命中最内层的容器:
function detectDropTarget(
mouseX: number,
mouseY: number,
containers: ContainerInfo[]
): ContainerInfo | null {
// 关键:按深度从深到浅排序,优先匹配最内层容器
const sorted = [...containers].sort(
(a, b) => b.depth - a.depth
);
for (const container of sorted) {
const { left, top, right, bottom } = container.rect;
if (
mouseX >= left && mouseX <= right &&
mouseY >= top && mouseY <= bottom
) {
// 还需检查该容器是否允许放置该组件
if (container.allowDrop) return container;
}
}
return null;
}
2. 插入位置计算
确定容器后,还需要计算在容器内的具体插入位置。对于流式布局,基于子元素中线判断;对于自由布局,直接使用坐标:
/** 流式布局 - 基于中线判断 */
function getInsertIndex(
children: DOMRect[],
mouseY: number,
direction: 'horizontal' | 'vertical'
): number {
for (let i = 0; i < children.length; i++) {
const rect = children[i];
const mid = direction === 'vertical'
? rect.top + rect.height / 2
: rect.left + rect.width / 2;
const mousePos = direction === 'vertical' ? mouseY : mouseY;
if (mousePos < mid) return i;
}
return children.length;
}
3. 辅助线与吸附
拖拽过程中需要显示辅助线帮助用户对齐:
- 边对齐:左/右/上/下边与兄弟元素对齐
- 中线对齐:水平/垂直中心线对齐
- 等间距:与相邻元素保持等间距
- 吸附阈值:当距离 < 5px 时自动吸附到参考线位置
Q3: 渲染引擎如何将 Schema 转化为真实的 UI 组件?如何处理性能问题?
答案:
渲染引擎的核心是递归遍历 Schema 树,将每个节点映射为对应的组件实例。
渲染流程:
性能优化策略:
| 策略 | 说明 |
|---|---|
| 组件懒加载 | 使用 React.lazy + Suspense,按需加载组件代码 |
| 局部更新 | 属性变更只重渲染对应子树,不全量更新 |
| 表达式缓存 | 缓存表达式编译结果,避免重复 parse |
| 虚拟滚动 | 长列表场景使用虚拟滚动,只渲染可见区域 |
设计态 vs 运行态的渲染差异:
| 维度 | 设计态 | 运行态 |
|---|---|---|
| 选中/悬停 | 显示组件边框和操作手柄 | 无额外 UI |
| 空容器 | 显示占位提示"拖入组件" | 不显示 |
| 事件 | 拦截点击,改为选中组件 | 正常触发业务事件 |
| 性能 | 允许稍慢(编辑体验) | 必须极致优化(用户体验) |
Q4: 如何设计低代码平台的出码能力?出码和直接渲染 Schema 各有什么优缺点?
答案:
出码指将 Schema 转换为可独立运行的源代码(如 React/Vue 项目),使低代码产物脱离平台运行。
出码引擎的核心设计:
出码引擎将 Schema 先转化为 AST(抽象语法树),再从 AST 生成代码字符串,最后用 Prettier 格式化。这种方式比直接拼字符串更可靠,也更容易支持多种输出格式。
两种方案对比:
| 维度 | Schema 渲染 | 出码 |
|---|---|---|
| 产物 | Schema JSON + 运行时渲染引擎 | 独立的源代码项目 |
| 更新 | 修改 Schema 即时生效 | 需要重新构建部署 |
| 性能 | 依赖运行时解析,有额外开销 | 编译期优化,性能更好 |
| 体积 | 需要打包渲染引擎(~100KB+) | 只包含使用到的组件代码 |
| 二次开发 | 只能通过平台配置 | 可以直接修改源码 |
| 调试 | 需要在平台内调试 | 使用标准开发工具 |
| SEO | 需要额外处理 | 可配合 SSR 方案 |
| 适用场景 | 营销页面、后台表单 | 正式项目、需要深度定制 |
成熟的低代码平台通常同时支持两种方案:日常迭代用 Schema 渲染快速上线;需要高性能或深度定制时用出码导出源代码。阿里的 LowCodeEngine 就采用了这种双轨策略。
相关链接
- 阿里低代码引擎 LowCodeEngine - 阿里开源的企业级低代码引擎
- 低代码引擎协议规范 - 阿里低代码物料协议规范
- dnd-kit - 现代化的 React 拖拽库
- Immer - 不可变数据操作库,适用于 Schema 管理
- 设计在线图片编辑器 - 涉及 Canvas 和撤销重做等相似架构
- 前端 SDK 通用架构设计 - 插件化架构设计思路
- 微前端架构 - Module Federation 与低代码的结合