Writing
拖拽引擎与 DSL:低代码平台的可扩展边界
低代码平台的核心不是画布,而是 DSL。DSL 设计得好,编辑器可以换、运行时可以换,资产还能沉淀。
低代码平台真正的核心
大多数低代码项目从画布开始做:拖一个按钮上去,能改颜色,能预览。做到第三个月就会发现,所有复杂度都堆在「这个组件能不能配这个属性」上——因为组件与配置是硬编码的,新增一个能力要改编辑器源码。
达芬奇平台在立项时把顺序倒了过来:先定义 DSL,再做编辑器,最后做编译器。回头看,这是整个项目最重要的决定。
DSL 设计:只描述结构,不描述表现
DSL 描述组件树、属性与数据绑定,不描述样式细节:
{
"version": "1.2",
"root": {
"id": "page",
"type": "Page",
"children": [
{
"id": "form_1",
"type": "SignupForm",
"props": { "title": "秋季招生", "fields": ["name", "phone"] },
"bindings": { "source": "api://campus/{campusId}/form" }
}
]
}
}
三条约定让 DSL 长期可维护:
version必须存在。平台升级时能识别旧结构并迁移,否则历史页面会集体失效。type对应组件注册表,而不是 import 路径。组件升级不改变 DSL。- 数据绑定与静态属性分离。
props是常量,bindings是表达式,编辑器与运行时各自处理。
拖拽引擎:难点在落点计算
拖拽的交互不难,难的是「拖到目标容器后,插入到第几个位置」。
我们放弃依赖浏览器 dragover 的触发顺序,改为自己算:
function resolveInsertIndex(
pointer: { x: number; y: number },
children: DOMRect[],
axis: 'x' | 'y',
): number {
for (let i = 0; i < children.length; i += 1) {
const rect = children[i];
const middle = axis === 'y' ? rect.top + rect.height / 2 : rect.left + rect.width / 2;
const value = axis === 'y' ? pointer.y : pointer.x;
if (value < middle) return i;
}
return children.length;
}
配合统一坐标系(滚动与缩放通过变换矩阵换算),拖拽精度稳定,跨容器拖动也不会漂移。
另一个关键点是状态分层:拖拽过程只更新临时状态,落点确定后才写入 DSL。这样整个画布不会在拖动时重渲染组件树,帧率稳定得多。
从运行时解释到编译时生成
第一版是运行时解释 DSL:浏览器里递归渲染组件树。问题很快暴露:
- 首屏要等 DSL 下载 + 解析 + 渲染,慢。
- 搜索引擎抓不到内容,SEO 为零。
- 页面无法被 CDN 缓存。
第二版改为编译时生成:
# 读取 DSL,生成静态页面与资源清单
node ./cli/build.js --dsl ./pages/autumn-2021.json --out ./dist
- Node CLI 解析 DSL,生成静态 HTML 与按需加载的资源清单。
- 产物直接部署,页面不再携带渲染引擎。
- 发布从「等运维」变成分钟级,且天然支持回滚到任意版本。
这次改造把一个「编辑时爽、运行时慢」的平台,变成了真正能承接流量的平台。
微前端:让三个团队各管一块
编辑器、组件库、发布控制台由不同小组维护。统一 DSL 与通信协议之后:
- 组件库只暴露注册表与属性 schema,不感知编辑器。
- 编辑器只操作 DSL,不 import 任何具体组件实现。
- 控制台只消费编译产物与版本信息。
跨团队联调成本下降约 30%,组件复用率提升约 40%。
结果
- 覆盖约 80% 的可视化页面搭建场景,运营可自助上线常规活动页。
- 同类页面从「每次重写」变为「选模板改配置」。
- 页面发布缩短到分钟级,前端排期压力明显缓解。
边界在哪里
低代码不是万能的。这套平台的适用边界很清晰:
- 适合:结构重复、字段驱动、活动周期短的表单与落地页。
- 不适合:强交互、强定制的产品页面(如复杂编辑器、实时协作)。
强行把后者塞进低代码,最终会得到一个比手写更难维护的 DSL。
复盘
如果把低代码平台当成一个编译器项目,很多决策会变得清晰:DSL 是源码,编辑器是 IDE,组件库是标准库,Node CLI 是编译器。四者通过契约连接,任意一部分都可以独立演进。
平台的价值不在于「让不会写代码的人写页面」,而在于把重复的页面结构变成可版本化、可复用的资产。