低代码平台最容易被误解的地方,是把它看成一个“拖拽搭页面”的工具。拖拽确实是用户最先感知到的能力,但平台能不能长期演进,真正取决于底下那份 DSL。
编辑器只是 DSL 的可视化编辑方式,Renderer 只是 DSL 的运行方式,发布系统只是 DSL 的版本管理方式。只要 DSL 设计失控,后面再强的编辑器也救不回来。
我更愿意把低代码营销平台理解成这样:
编辑器层 -> 生产 DSL协议层 -> 约束 DSL渲染层 -> 消费 DSL发布层 -> 管理 DSL 版本数据采集层 -> 根据 DSL 执行埋点和转化统计营销自动化层 -> 根据 DSL 中的事件和数据触发后续动作
也就是说,DSL 不是一个中间文件,而是平台的产品边界。
DSL 不能只是组件列表
最初做低代码页面时,DSL 很容易从一个组件列表开始:
{ "pageId": "landing_001", "components": [ { "id": "hero_1", "type": "HeroBanner", "props": { "title": "618 大促", "subtitle": "限时优惠" } } ]}
这对静态页面足够。但营销平台很快会遇到更多需求:
组件需要调用接口按钮点击需要跳转和埋点表单提交后需要通知销售页面需要灰度版本不同角色看到不同配置组件需要绑定变量部分模块需要按条件展示AI 要根据自然语言生成页面
这时 DSL 如果还只是 components + props,就会被迫往 props 里塞各种额外字段。短期看能跑,长期看会变成一坨难以解释的 JSON。
更完整的页面 DSL 至少应该包含几类信息:
| 模块 | 说明 |
|---|---|
page | 页面基础信息,如标题、路由、渠道、SEO |
components | 组件树和组件属性 |
dataSources | 页面级数据源定义 |
variables | 页面变量和运行时上下文 |
events | 页面级或组件级事件编排 |
permissions | 编辑、发布、访问权限规则 |
tracking | PV、曝光、点击、转化等采集配置 |
version | DSL、组件和运行时版本 |
低代码平台后期能不能接入 Workflow、AI、多端渲染和营销自动化,基本都取决于这些字段有没有清楚的位置。
页面结构和组件协议要分开
页面 DSL 描述的是“这个页面用了什么组件,以及它们如何组合”。组件协议描述的是“某类组件支持什么属性、事件、数据和埋点”。
这两层不能混在一起。
组件协议可以是:
export const ButtonMeta = { type: "Button", title: "按钮", category: "basic", propsSchema: { text: { type: "string", title: "按钮文案", required: true }, link: { type: "string", title: "跳转链接" }, size: { type: "enum", title: "尺寸", options: ["small", "middle", "large"] } }, events: ["onClick"]};
页面 DSL 中使用这个组件:
{ "id": "button_1", "type": "Button", "props": { "text": "立即领取", "link": "app://coupon/detail?id=123", "size": "large" }, "events": { "onClick": [ { "type": "track", "event": "coupon_button_click" }, { "type": "openUrl", "url": "{{props.link}}" } ] }}
组件协议像类型定义,页面 DSL 像实例数据。类型定义可以升级,实例数据需要稳定保存。这样编辑器、运行时和发布系统才能围绕同一套契约协作。
DSL 要能表达事件,而不只是属性
营销页面天然有行为:点击按钮、提交表单、领取优惠券、跳转页面、打开弹窗、调用接口、上报埋点。
如果这些逻辑写死在组件里,页面的可配置性会很差。比如同一个按钮,在 A 页面点击后跳转商品详情,在 B 页面点击后打开表单,在 C 页面点击后触发优惠券领取。如果每个场景都改组件代码,低代码的价值就消失了。
因此事件应该进入 DSL:
{ "event": "onClick", "actions": [ { "type": "track", "event": "button_click", "params": { "module": "hero" } }, { "type": "request", "dataSource": "coupon.receive" }, { "type": "toast", "message": "领取成功" } ]}
这套结构本质上是一个轻量 Workflow。它不一定要做得很复杂,但至少要能表达:
- 触发条件。
- 动作顺序。
- 动作参数。
- 成功和失败分支。
- 是否阻塞后续动作。
- 是否需要埋点。
这比组件内部写死 onClick 更适合营销平台。
数据绑定要有统一表达方式
营销页面不只是静态页面。商品卡、车型列表、优惠券、用户信息、门店卡、直播预约,都可能来自动态数据源。
页面 DSL 可以定义数据源:
{ "dataSources": { "productList": { "type": "api", "url": "/api/products", "method": "GET", "cache": "page" } }}
组件消费数据时,不应该直接写请求逻辑,而是绑定变量:
{ "type": "ProductList", "props": { "items": "{{dataSources.productList.data}}" }}
这样运行时可以统一处理请求、缓存、错误、loading、重试和首屏优先级。组件只关心自己拿到的 items,不关心它来自 REST API、GraphQL、商品系统还是 CRM。
数据绑定的关键是边界清楚:
DataSource 负责取数Expression 负责取值和计算Component 负责展示Renderer 负责把三者串起来
如果组件自己到处发请求,页面就很难做统一性能优化和异常治理。
表达式引擎要克制
低代码平台通常都会需要表达式:
{{ user.name }}{{ dataSources.productList.data[0].price }}{{ product.price > 100 ? "高客单" : "普通" }}
表达式能让配置更灵活,但也容易失控。尤其是允许用户写任意 JavaScript 时,安全性、性能和可调试性都会变差。
更稳妥的做法是先提供受限表达式:
路径读取:{{ user.name }}简单比较:{{ user.level === "vip" }}简单三元:{{ price > 100 ? "高客单" : "普通" }}内置函数:{{ formatPrice(product.price) }}
实现上可以分阶段:
第一阶段:jsonpath / lodash.get第二阶段:受限表达式 parser第三阶段:沙箱执行少量白名单函数
不要一开始就把完整 JS 执行能力暴露给 DSL。低代码平台的表达式不是为了变成编程语言,而是为了让配置可以引用上下文和做少量计算。
DSL 要有版本
DSL 一旦进入生产,就必须考虑版本。
至少要区分三类版本:
页面 DSL 版本组件协议版本运行时 Renderer 版本
页面 DSL 版本用于发布和回滚:
{ "pageId": "landing_001", "version": 12}
组件协议版本用于确定组件属性如何解释:
{ "type": "CouponCard", "componentVersion": "2.1.0"}
运行时版本用于兼容不同 DSL 能力:
{ "runtimeVersion": "1.5.0"}
没有版本的 DSL 很难长期维护。因为你无法判断一个历史页面应该用新协议解释,还是继续按旧协议运行。
AI 生成页面依赖 DSL 的稳定性
AI 接入低代码平台时,最自然的方式不是让 AI 直接写 React 代码,而是让 AI 生成 DSL。
链路可以是:
用户 Prompt -> LLM 生成页面 DSL -> Schema 校验 -> 自动补默认值 -> Renderer 渲染 -> 用户在编辑器中继续调整
例如用户输入:
帮我生成一个双十一促销页,有头图、优惠券、商品列表和留资表单。
AI 输出:
{ "components": [ { "type": "HeroBanner" }, { "type": "CouponCard" }, { "type": "ProductList" }, { "type": "LeadForm" } ]}
这里的前提是 DSL 足够稳定、组件协议足够清楚。AI 不是绕过低代码体系,而是成为 DSL 的另一个生产入口。
如果 DSL 本身混乱,AI 生成的东西也只能是混乱配置。
小结
低代码平台真正难的不是拖拽,而是 DSL 长期不失控。
一份好的 DSL 应该能稳定描述:
- 页面结构。
- 组件类型和属性。
- 事件和动作。
- 数据源和变量绑定。
- 权限和版本。
- 埋点和转化。
- AI 生成后的校验边界。
很多平台后期变得难维护,不是因为 React 写得差,也不是因为拖拽库选错了,而是因为 DSL 从一开始没有清楚边界。低代码平台越往后做,越会发现 DSL 才是真正的产品核心。