本文基于我参与开发的低代码平台项目,总结如何进行架构设计与技术选型,适合准备落地低代码编辑器,或参与前端架构设计的开发者阅读。
一、项目背景与设计目标:低代码的意义到底是什么?
📌 什么是低代码?
低代码是一种软件开发方法,只需很少的手工编码,通过可视化的方式 , 通过编排和配置组件来生产页面的开发方法
🎯 它的真正价值是什么?
低代码的意义从来不在于“一行代码都不写” ,而是让开发者尽可能少地写代码,同时降低重复性工作带来的风险。
试想一下这样的场景:
-
常用功能(如弹窗、表单、列表)已经实现过 N 次;
-
每次还是要 patch 一些边角 bug;
-
测试由于“看起来很简单”,没仔细测;
-
最后上线,出现了事故。
低代码通过“内置常见功能”,把这类重复性劳动封装为平台能力,从而降低:
- 开发成本
- 测试成本
- 交付风险
它让交付质量不再依赖开发者当下的经验、精力或注意力水平,这是笔者认为 现阶段低代码平台最大的价值。
🧱 二、系统模块构成(平台四大核心)
大部分低码平台,主要都是由以下四部分组成:
🏗️ 三、架构设计原则:JSON Schema驱动 + 解耦设计
- 整体采用数据驱动的思想,定义低代码中的 JSON 数据结构,将每个组件抽象成一个 JSON 结构的对象
- 易于导入导出、保存撤销等操作
- 实现自定义的 render 函数,将拖拽到预览区的组件,转化成 JSON 对象的新增,再将 JSON 对象通过 render 函数动态渲染生成 ui 组件。实现了组件的动态渲染与可扩展性,方便后续物料区组件模版的新增。
- 通过配置区,实现了 JSON 数据项的动态配置,从而动态改变 ui 的渲染,实现组件的可定制化。
- 对比JSON 数据项实现diff算法,实现撤销和重做
🔧 四、技术选型分析
| 模块 | 选型 | 原因 |
|---|---|---|
| UI 渲染 | React + Hooks | 组件化、响应式、生态成熟 |
| 拖拽实现 | 原生 Drag API + Hook | 保持轻量,可深度自定义行为,避免大体积库 |
| 状态管理 | Zustand | 简洁无模板约束,易于记录快照、支持中间件 |
| 快照机制 | 自定义 diff | 性能更优于 JSON.parse/JSON.stringify 深拷贝 |
| 样式方案 | sass支持css变量,嵌套,减少代码量 | 支持主题切换、组件样式封装更方便 |
| 部署 | Vercel | 支持 Git 自动部署,CI/CD 敏捷开发 |
🔄 五、功能亮点总结
撤销 / 重做机制设计
1. 基本原理
在编辑器中,为了实现撤销(Undo)与重做(Redo)功能,需要记录用户的每一步操作。最简单的方式是:
- 维护快照数组:保存每次操作后的完整编辑器数据 (
componentData)。 - 维护当前索引:指向当前快照位置。
- 撤销:索引
-1→ 取对应快照 → 更新画布。 - 重做:索引
+1→ 取对应快照 → 更新画布。
这种方式虽然直观,但会出现明显问题:
快照是完整深拷贝,即snapshot:componentData[][],数据冗余严重,内存占用高。
2. 数据结构优化
更高效的做法是记录操作差异(diff)而不是整份数据,并用结构化对象描述:
export interface HistoryProps {
id: string;
componentId: string;
type: 'add' | 'delete' | 'modify';
data: any; // { [key]: { oldValue, newValue } }
index?: number; // 组件在列表中的位置(添加/删除操作用)
}
✅ 优势
- 存储更轻量:只记录变化字段,不保存整份组件数据。
- 定位更精准:可以快速找到变更位置。
- 回滚逻辑更明确:撤销只需执行反操作,而不是替换整份数据。
3. 快照回滚机制
-
撤销(Undo)
-
从历史记录栈中取出最近一条操作 → 执行反操作
-
反操作定义:
add→ 删除该组件delete→ 恢复该组件modify→ 用旧值覆盖当前值
-
-
重做(Redo)
- 从撤销栈中取出一条记录 → 再次执行该操作
Diff 生成算法设计
Diff 算法用于比较旧组件数组与新组件数组,找出它们之间的差异,并用统一的数据结构(DiffOperation)描述,为撤销/重做提供基础。
实现步骤
2.1. 建立映射 时间复杂度从O(n2)降低到O(n)
- 将旧数组和新数组转为
Map<id, component>,快速按 id 查找。
2.2. 找删除项
- 遍历旧数组,如果某个 id 在新数组中不存在 → 记录
DELETEdiff。
2.3. 找新增项
- 遍历新数组,如果某个 id 在旧数组中不存在 → 记录
ADDdiff。
2.4. 找修改项
-
对于新旧数组中都存在的组件:
- 比较顶层属性(除 id、style)→ 若不同,记录
MODIFYdiff。 - 比较 style 属性 → 若数值差异大于 0.1(避免浮点误差),记录
MODIFY diff,并用style.width这种形式标识字段。
- 比较顶层属性(除 id、style)→ 若不同,记录
2.5. 返回结果
- 收集所有 diff 操作 → 输出 diff 数组。
伪代码
这种实现方式最通俗易懂,但是需要三轮遍历数组。
for (每个旧组件) {
if (新组件中没有) -> 生成 DELETE diff
}
for (每个新组件) {
if (旧组件中没有) -> 生成 ADD diff
}
for (id 相同的组件) {
比较属性和 style,生成 MODIFY diff
}
将三轮遍历优化成一轮
读过react源码的同学都知道,react 的 diff 算法在同一层级的节点比较中通常只需要一轮遍历。
React 的 Diff 算法核心流程如下:
- 第一轮:头尾同步扫描
- oldList/newList 从头到尾同步遍历,只要 key/type 都一样就复用,遇到key不同就 break。
- 第二轮:处理剩余节点
- oldList 剩余的全部删除,newList 剩余的全部新增。
- 移动节点优化
- 如果 old/new 都有剩余,说明有节点顺序变化。
- 用 key 建立映射,找出最长递增子序列(LIS),最小化移动次数。
第一轮
let oldIndex = 0;
let newIndex = 0;
while (oldIndex < oldList.length && newIndex < newList.length) {
if (key 和 type 都一样) {
// ① key 和 type 都一样 → 复用节点,比较属性是否相同->type:修改
比较属性是否相同(oldList[oldIndex], newList[newIndex]);
oldIndex++;
newIndex++;
} else {
// ② 一旦 key 或 type 不同 → 停止第一轮,进入第二轮
break;
}
}
第二轮
// oldList 剩余的节点 → 需要删除 type:删除
for (let i = oldIndex; i < oldList.length; i++) {
markForDelete(oldList[i]);
}
// newList 剩余的节点 → 需要新增 type:新增
for (let j = newIndex; j < newList.length; j++) {
markForAdd(newList[j]);
}
//目前低代码平台并没有移动节点的case,以下部分仅了解,不需要实现
// 如果 oldList 和 newList 都有剩余节点→ 需要移动
// → 说明是 "节点位置发生了变化"
// React 会用 key 做映射,并通过 最长递增子序列 (LIS)算法,计算最少的移动次数
if (hasRemainingOldNodes && hasRemainingNewNodes) {
mapOldNodesByKey();
findLISandGenerateMoveDiffs();
}
previousData 如何获取?
编辑器在每次更新时只能拿到最新的 componentData,没有直接存储上一版本(usePrevious可以获取上一版本,但只能撤销一次,不能撤销多次)。
解决方法:动态回放快照。
- 编辑器挂载时,拿到初始数据
- 从初始数据开始,依次应用历史快照中的 diff(
applyDiff) - 得到当前索引对应的
previousComponents
这样可以 动态计算出任意版本的组件状态,无需存储所有完整快照。
参考资料:
阿里低代码引擎B站视频 www.bilibili.com/video/BV1gu…
低代码渲染那些事:juejin.cn/post/711904…
关于前端低代码的一些个人观点 juejin.cn/post/713180…
《从0到1实现低代码平台》超详细教程!juejin.cn/post/713897…
低代码平台开发实践:基于React: mp.weixin.qq.com/s/ER6P0FLsD…