第四章:核心数据结构设计
好,咱们今天聊聊核心数据结构。说实话,这部分是可视化编辑器的灵魂。我见过太多项目,界面做得花里胡哨,结果底层数据结构一塌糊涂,最后改需求改到崩溃。嗯,咱们得从一开始就把地基打牢。
4.1 组件树(Component Tree)
组件树是什么?说白了,就是页面里所有组件的层级关系。你想想看,一个页面就像一棵树,根节点是页面本身,子节点是各种容器和组件。
核心要点:组件树必须支持嵌套、排序、增删改查。这是最基本的操作。
我在项目中遇到过最头疼的事,就是组件树和实际渲染不一致。后来我总结了一套规则:
// 组件树节点定义
interface TreeNode {
id: string; // 唯一标识
type: string; // 组件类型,比如 'Button', 'Container'
props: Record<string, any>; // 组件属性
children: TreeNode[]; // 子节点列表
parentId?: string; // 父节点ID,方便向上查找
}
为什么要加 parentId?因为在实际操作中,你经常需要从子节点找到父节点。比如删除一个组件,你得通知父节点更新 children 列表。没有 parentId,你就得递归遍历整棵树,性能太差了。
我的习惯:组件树用 Map 存储,key 是 id,value 是节点。这样查找任意节点都是 O(1) 复杂度。别用数组,数组查找太慢了。
4.2 属性面板数据模型
属性面板,就是用户选中组件后,右侧弹出的那个编辑区域。这里的数据模型设计,直接决定了编辑器的易用性。
我曾经踩过一个坑:把所有属性都放在一个平铺的对象里。结果组件一多,属性面板乱成一锅粥。后来我改用分组结构:
// 属性面板数据模型
interface PropertyGroup {
title: string; // 分组标题,比如 '基础属性', '样式属性'
properties: PropertyDef[];
}
interface PropertyDef {
key: string; // 属性名,对应组件 props 里的字段
label: string; // 显示名称,比如 '背景颜色'
type: 'string' | 'number' | 'boolean' | 'color' | 'select';
defaultValue?: any; // 默认值
options?: { label: string; value: any }[]; // 下拉选项
validation?: (value: any) => boolean; // 校验函数
}
你看,这样分组后,属性面板可以按组折叠展开。用户找属性也方便,开发者也容易维护。
注意:属性面板的数据模型,要和组件树里的 props 字段一一对应。我曾经见过有人用两套不同的命名,结果调试时对不上,浪费了好几天时间。
4.3 页面 Schema 定义
页面 Schema,就是整个页面的完整描述。它包含了组件树、全局配置、样式变量、事件绑定等等。说白了,你保存一个页面,实际上就是保存这个 Schema。
我建议用这样的结构:
// 页面 Schema
interface PageSchema {
version: string; // Schema 版本号,方便后续升级
page: {
title: string; // 页面标题
width: number; // 画布宽度
height: number; // 画布高度
backgroundColor: string;
};
components: TreeNode[]; // 组件树根节点列表
globalStyles: Record<string, any>; // 全局样式变量
events: EventBinding[]; // 事件绑定列表
dataSources: DataSource[]; // 数据源配置
}
为什么要加 version?因为随着编辑器迭代,Schema 结构可能会变。有了版本号,你可以写迁移脚本,把旧版本的 Schema 转成新版本。我在项目中就靠这个机制,平滑升级了三次大版本。
核心原则:Schema 要能完整还原页面。也就是说,你给我一个 Schema,我能渲染出完全一样的页面。少一个字段都不行。
4.4 JSON Schema 规范
JSON Schema 是啥?它是用来描述 JSON 数据结构的规范。咱们的组件属性、页面配置,本质上都是 JSON。用 JSON Schema 来定义它们,好处太多了:
- 自动校验:用户输入不合法的值,直接报错
- 自动生成表单:根据 Schema 生成属性面板的输入控件
- 文档即代码:Schema 本身就是文档,不用额外写说明
举个例子,一个按钮组件的属性 Schema:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"text": {
"type": "string",
"title": "按钮文字",
"default": "按钮"
},
"type": {
"type": "string",
"title": "按钮类型",
"enum": ["primary", "default", "dashed", "text"],
"default": "default"
},
"size": {
"type": "string",
"title": "按钮大小",
"enum": ["large", "middle", "small"],
"default": "middle"
},
"disabled": {
"type": "boolean",
"title": "是否禁用",
"default": false
}
},
"required": ["text"]
}
你看,这个 Schema 定义了按钮的所有属性。编辑器拿到这个 Schema,就能自动生成属性面板。用户修改属性时,还能实时校验。
我的经验:JSON Schema 的 draft-07 版本最常用。它支持条件校验、引用复用,基本够用了。别用太新的版本,兼容性不好。
4.5 数据结构的联动关系
这几个数据结构不是孤立的。它们之间有着紧密的联动:
| 数据结构 | 作用 | 关联关系 |
|---|---|---|
| 组件树 | 描述页面层级 | 每个节点的 props 由属性面板控制 |
| 属性面板模型 | 定义可编辑属性 | 根据组件类型,从 JSON Schema 生成 |
| 页面 Schema | 完整页面描述 | 包含组件树、全局配置等 |
| JSON Schema | 属性校验和表单生成 | 为属性面板提供元数据 |
举个例子,用户拖入一个按钮组件:
- 编辑器根据按钮的 JSON Schema,生成属性面板
- 用户在属性面板修改文字,更新组件树中对应节点的 props
- 编辑器重新渲染,按钮显示新文字
- 保存时,整个组件树序列化成页面 Schema
避坑指南:我曾经在项目中把组件树和属性面板的数据分开存储,结果用户修改属性后,组件树没更新,导致渲染和实际数据不一致。记住,组件树是唯一数据源,属性面板只是它的编辑器。
4.6 实战建议
最后,给你几个实战建议:
- 先画图,再写代码:用思维导图把数据结构画出来,确认没问题再动手
- 写单元测试:每个数据结构的增删改查都要测试,特别是边界情况
- 版本控制:Schema 的每个版本都要存档,方便回滚
- 性能优化:组件树超过 1000 个节点时,考虑虚拟滚动和增量更新
嗯,核心数据结构就聊这么多。这部分虽然枯燥,但真的重要。你想想看,数据结构设计好了,后面的开发就是一马平川。设计不好,天天改 bug 改到怀疑人生。我当年就是吃了这个亏,现在每次做新项目,第一件事就是先把数据结构定死。
下一章咱们聊聊渲染引擎,怎么把组件树变成真正的页面。到时候见。