打造一个可拖拽低代码平台的思考

506 阅读7分钟

本文基于我参与开发的低代码平台项目,总结如何进行架构设计与技术选型,适合准备落地低代码编辑器,或参与前端架构设计的开发者阅读。

一、项目背景与设计目标:低代码的意义到底是什么?

📌 什么是低代码?

低代码是一种软件开发方法,只需很少的手工编码,通过可视化的方式 通过编排配置组件来生产页面的开发方法

🎯 它的真正价值是什么?

低代码的意义从来不在于“一行代码都不写” ,而是让开发者尽可能少地写代码,同时降低重复性工作带来的风险。

试想一下这样的场景:

  • 常用功能(如弹窗、表单、列表)已经实现过 N 次;

  • 每次还是要 patch 一些边角 bug;

  • 测试由于“看起来很简单”,没仔细测;

  • 最后上线,出现了事故。

低代码通过“内置常见功能”,把这类重复性劳动封装为平台能力,从而降低:

  • 开发成本
  • 测试成本
  • 交付风险

它让交付质量不再依赖开发者当下的经验、精力或注意力水平,这是笔者认为 现阶段低代码平台最大的价值

🧱 二、系统模块构成(平台四大核心)

大部分低码平台,主要都是由以下四部分组成:

🏗️ 三、架构设计原则:JSON Schema驱动 + 解耦设计

  1. 整体采用数据驱动的思想,定义低代码中的 JSON 数据结构,将每个组件抽象成一个 JSON 结构的对象
  • 易于导入导出、保存撤销等操作
  1. 实现自定义的 render 函数,将拖拽到预览区的组件,转化成 JSON 对象的新增,再将 JSON 对象通过 render 函数动态渲染生成 ui 组件。实现了组件的动态渲染与可扩展性,方便后续物料区组件模版的新增。
  2. 通过配置区,实现了 JSON 数据项的动态配置,从而动态改变 ui 的渲染,实现组件的可定制化。
  3. 对比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. 快照回滚机制

  1. 撤销(Undo)

    • 从历史记录栈中取出最近一条操作 → 执行反操作

    • 反操作定义:

      • add → 删除该组件
      • delete → 恢复该组件
      • modify → 用旧值覆盖当前值

截屏2025-07-26 13.11.57.png

  1. 重做(Redo)

    • 从撤销栈中取出一条记录 → 再次执行该操作

截屏2025-07-26 13.12.13.png


Diff 生成算法设计

Diff 算法用于比较旧组件数组新组件数组,找出它们之间的差异,并用统一的数据结构(DiffOperation)描述,为撤销/重做提供基础。

截屏2025-07-26 13.12.49.png


实现步骤

2.1. 建立映射 时间复杂度从O(n2)降低到O(n)

  • 将旧数组和新数组转为 Map<id, component>,快速按 id 查找。

2.2. 找删除项

  • 遍历旧数组,如果某个 id 在新数组中不存在 → 记录 DELETE diff。

2.3. 找新增项

  • 遍历新数组,如果某个 id 在旧数组中不存在 → 记录 ADD diff。

2.4. 找修改项

  • 对于新旧数组中都存在的组件:

    • 比较顶层属性(除 id、style)→ 若不同,记录 MODIFY diff。
    • 比较 style 属性 → 若数值差异大于 0.1(避免浮点误差),记录 MODIFY diff,并用 style.width 这种形式标识字段。

2.5. 返回结果

  • 收集所有 diff 操作 → 输出 diff 数组。

伪代码

这种实现方式最通俗易懂,但是需要三轮遍历数组。

    for (每个旧组件) {
      if (新组件中没有) -> 生成 DELETE diff
    }
    for (每个新组件) {
      if (旧组件中没有) -> 生成 ADD diff
    }
    for (id 相同的组件) {
      比较属性和 style,生成 MODIFY diff
    }

将三轮遍历优化成一轮

读过react源码的同学都知道,react 的 diff 算法在同一层级的节点比较中通常只需要一轮遍历

React 的 Diff 算法核心流程如下

  1. 第一轮:头尾同步扫描
  • oldList/newList 从头到尾同步遍历,只要 key/type 都一样就复用,遇到key不同就 break。
  1. 第二轮:处理剩余节点
  • oldList 剩余的全部删除,newList 剩余的全部新增。
  1. 移动节点优化
  • 如果 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();
}


image.png

previousData 如何获取?

编辑器在每次更新时只能拿到最新的 componentData,没有直接存储上一版本(usePrevious可以获取上一版本,但只能撤销一次,不能撤销多次)。
解决方法:动态回放快照

  • 编辑器挂载时,拿到初始数据
  • 从初始数据开始,依次应用历史快照中的 diff(applyDiff
  • 得到当前索引对应的 previousComponents

这样可以 动态计算出任意版本的组件状态,无需存储所有完整快照。

参考资料:

阿里低代码引擎B站视频 www.bilibili.com/video/BV1gu…

文档 lowcode-engine.cn/index

低代码渲染那些事:juejin.cn/post/711904…

关于前端低代码的一些个人观点 juejin.cn/post/713180…

《从0到1实现低代码平台》超详细教程!juejin.cn/post/713897…

低代码平台开发实践:基于React: mp.weixin.qq.com/s/ER6P0FLsD…