所见即所得低代码编辑器画布:编辑层与渲染层分离方案

在开发或使用低代码产品时,"预览"是一个绕不开的功能。市面上的编辑器通常有两种实现:

  1. 打开一个新窗口进行预览
  2. 直接在编辑区所见即所得

用户当然选第 2 种。但开发者知道,之所以很多编辑器选择了第 1 种,是因为所见即所得非常依赖实际生产的运行环境。很多声称"所见即所得"的编辑器,实际只是有相似度的近似还原——编辑环境和生产环境差距太大,预期外的问题总会出现。

这篇文章介绍我是如何通过编辑层与渲染层分离的方案,实现真正意义上的所见即所得。

问题:传统画布为什么做不到

通常的画布实现

低代码编辑器中,通常是把组件拖拽到画布中,展示页面布局。点击元素会弹出属性配置面板,修改配置后效果在画布中更新。

主流编辑器的做法是将拖拽元素、选择事件、选框都写在同一个文档流中。这意味着画布包含了编辑器自身的样式和脚本,而生产环境并没有这些内容。

结论:在这种设计下,编辑器中的内容必然无法完全做到所见即所得。

通常的预览实现

既然画布做不到所见即所得,就衍生出了"弹出单独容器"的预览方式——可能是沙盒窗口,也可能是新页面。

这虽然能达到预览目的,但编辑和预览之间产生了割裂:一边编辑,还要一边切换窗口看效果,显得笨拙。

核心思路:把预览垫在画布下面

由此不难想到:如果画布上编辑的内容就是预览的内容,不就能做到直接编辑生产环境了吗?

思路:把预览窗口垫在画布区域下面,每当编辑器中的页面产生变化,就重新渲染预览容器。

但直接叠放会有两个显而易见的问题:

  • 组件的展示会产生重叠
  • 两层内容会产生错位

方案:编辑层 + 渲染层

针对这两个问题,我设计了一套多层结构画布,由编辑层渲染层组成:

渲染层:零侵入

渲染层就是预览页面本身。在这一层不做任何额外工作,因为任何改动都会影响生产版本的还原度。

选择 iframe 作为容器,原因:

  • 完美隔绝编辑器和渲染层的上下文,不受意外干扰
  • 弹窗类组件的全屏蒙版更可控

编辑层:透明触发器

编辑层相比传统画布的最大区别是:不再感知组件实体

取而代之的是一组通用占位组件——透明的触发器,遮盖在渲染层对应组件的上方。每个触发器绑定了对应组件的信息,用户点击时,实际触发的是编辑行为。

用户操作流程

  1. 用户看到的是渲染层展示的真实页面
  2. 点击某个组件时,实际点到的是编辑层的透明触发器
  3. 触发器根据绑定的组件信息,打开属性配置面板
  4. 编辑完成后,将产物传给渲染层重新渲染
  5. 用户立即看到生产环境级别的更新效果

关键难题:布局同步

用户点击画布中的组件时,系统如何正确识别点击目标?这需要编辑层的触发器和渲染层的组件精确对齐

失败的尝试:自然继承

我最初尝试的方案是:收集渲染层组件的实际尺寸,让编辑层触发器继承相同的布局模式(flex 对 flex,grid 对 grid),自然形成对齐。

这个方案很快失败了:

  • box-sizing 在两层定义不同时,整个布局逻辑都不一样
  • 组件自定义样式是黑盒,编辑层无法完全感知

成功的方案:DOMRect 精确定位

转向另一个思路:直接获取渲染层组件的精确位置信息

在 DSL 设计时,每个节点都有唯一 ID,这个 ID 会附着在渲染组件的 DOM 上。通过 getBoundingClientRect() 获取 DOMRect 对象,结合 overflow 属性,就能精确定位每个组件在渲染层的实际位置和大小。

因为渲染层使用 iframe,编辑器可以访问渲染层的上下文,所以能轻松获取所有组件的布局信息,并与编辑层一一对应。

// 获取渲染层中每个组件的精确位置
function syncLayout(iframeDoc: Document, nodeIds: string[]) {
  return nodeIds.map(id => {
    const el = iframeDoc.querySelector(`[data-node-id="${id}"]`)
    if (!el) return null
    const rect = el.getBoundingClientRect()
    const overflow = getComputedStyle(el).overflow
    return { id, rect, overflow }
  })
}

额外收益

编辑层和渲染层分离不仅解决了所见即所得问题,还带来了意想不到的好处:

跨技术栈支持

渲染层的隔离就是组件运行环境的隔离。稍加改造,可以让渲染层以外的部分不感知组件的技术实现。

比如:React 开发的编辑器,拖拽 Vue 开发的组件库。编辑器的可复用性大幅提升。

线上页面调试工具

反过来想:在已有的生产页面上植入编辑层,就可以做一个 Chrome 扩展,实现"在线可视化调试"——直接在线上页面上选择组件、查看属性、调整配置。

已知局限

这个方案也有不足之处:

局限说明
知识成本能否完美还原依赖组件库开发者的建设,所有生产依赖的脚本和样式都需要在渲染层正确加载
动态页面静态页面天然适配,但涉及接口调用的动态页面需要额外的数据绑定机制
图文混排行内片段可能是多边形,选框不够精准
渲染性能双层结构导致重新渲染次数偏多,需要策略性的变更监听来减少回流

总结

低代码编辑器的"所见即所得"一直是个挑战。通过将画布拆分为编辑层和渲染层——渲染层用 iframe 隔离出真实的生产环境,编辑层用透明触发器覆盖其上——可以在不侵入组件运行环境的前提下,实现接近 100% 的所见即所得效果。

当前阿里的低代码引擎和腾讯的魔方平台也采用了类似方案。不同的是,两个大厂选择了定制化画布,而这里给出的方案更加开放——增加了一些开发知识点,但把掌控权交给了开发者。

Comments