在开发或使用低代码产品时,"预览"是一个绕不开的功能。市面上的编辑器通常有两种实现:
- 打开一个新窗口进行预览
- 直接在编辑区所见即所得
用户当然选第 2 种。但开发者知道,之所以很多编辑器选择了第 1 种,是因为所见即所得非常依赖实际生产的运行环境。很多声称"所见即所得"的编辑器,实际只是有相似度的近似还原——编辑环境和生产环境差距太大,预期外的问题总会出现。
这篇文章介绍我是如何通过编辑层与渲染层分离的方案,实现真正意义上的所见即所得。
问题:传统画布为什么做不到
通常的画布实现
低代码编辑器中,通常是把组件拖拽到画布中,展示页面布局。点击元素会弹出属性配置面板,修改配置后效果在画布中更新。
主流编辑器的做法是将拖拽元素、选择事件、选框都写在同一个文档流中。这意味着画布包含了编辑器自身的样式和脚本,而生产环境并没有这些内容。
结论:在这种设计下,编辑器中的内容必然无法完全做到所见即所得。
通常的预览实现
既然画布做不到所见即所得,就衍生出了"弹出单独容器"的预览方式——可能是沙盒窗口,也可能是新页面。
这虽然能达到预览目的,但编辑和预览之间产生了割裂:一边编辑,还要一边切换窗口看效果,显得笨拙。
核心思路:把预览垫在画布下面
由此不难想到:如果画布上编辑的内容就是预览的内容,不就能做到直接编辑生产环境了吗?
思路:把预览窗口垫在画布区域下面,每当编辑器中的页面产生变化,就重新渲染预览容器。
但直接叠放会有两个显而易见的问题:
- 组件的展示会产生重叠
- 两层内容会产生错位
方案:编辑层 + 渲染层
针对这两个问题,我设计了一套多层结构画布,由编辑层和渲染层组成:
渲染层:零侵入
渲染层就是预览页面本身。在这一层不做任何额外工作,因为任何改动都会影响生产版本的还原度。
选择 iframe 作为容器,原因:
- 完美隔绝编辑器和渲染层的上下文,不受意外干扰
- 弹窗类组件的全屏蒙版更可控
编辑层:透明触发器
编辑层相比传统画布的最大区别是:不再感知组件实体。
取而代之的是一组通用占位组件——透明的触发器,遮盖在渲染层对应组件的上方。每个触发器绑定了对应组件的信息,用户点击时,实际触发的是编辑行为。
用户操作流程
- 用户看到的是渲染层展示的真实页面
- 点击某个组件时,实际点到的是编辑层的透明触发器
- 触发器根据绑定的组件信息,打开属性配置面板
- 编辑完成后,将产物传给渲染层重新渲染
- 用户立即看到生产环境级别的更新效果
关键难题:布局同步
用户点击画布中的组件时,系统如何正确识别点击目标?这需要编辑层的触发器和渲染层的组件精确对齐。
失败的尝试:自然继承
我最初尝试的方案是:收集渲染层组件的实际尺寸,让编辑层触发器继承相同的布局模式(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% 的所见即所得效果。
当前阿里的低代码引擎和腾讯的魔方平台也采用了类似方案。不同的是,两个大厂选择了定制化画布,而这里给出的方案更加开放——增加了一些开发知识点,但把掌控权交给了开发者。