Skip to content
Go back

当时做营销搭建器,我先想清楚的是三列结构

做营销活动页面时,业务诉求通常变化很快:今天要上线一套车型专题页,明天要接一个运营活动,后天又要改头图、轮播、车型列表和埋点规则。如果每次都走完整研发发布流程,效率很难满足业务节奏。

低代码营销平台要解决的不是“让所有人都能写页面”,而是把高频、重复、结构相对稳定的页面生产流程抽象出来。研发提供组件、协议和运行时,运营或业务同学通过拖拽和配置完成页面拼接。

一个典型的编辑器界面可以拆成三列:

左侧:组件库中间:页面预览区域右侧:组件属性编辑面板

中间预览区域使用 iframe 承载真实页面。用户从组件库中拖拽头图、轮播、车型列表等组件到 iframe 中,选中某个组件后,在右侧编辑它的基础参数、异步 API、埋点信息、必填项、默认值等配置。

这类平台的复杂度并不在 UI 三列布局,而在于:组件如何标准化、编辑器如何和预览页通信、配置如何驱动属性面板、开发者如何独立开发组件、最终页面如何在运行时高性能渲染。

整体架构要拆成编辑器、协议和运行时

低代码平台最好不要做成一个巨大的前端应用。更合理的拆法是三层:

编辑器 Editor  -> 负责拖拽、选中、排序、属性编辑、页面配置管理组件协议 Schema  -> 描述组件元信息、属性表单、默认值、数据源、埋点配置页面运行时 Runtime  -> 根据页面 DSL 加载组件并渲染出最终页面

编辑器面向“生产页面的人”,运行时面向“访问页面的用户”。两者关注点完全不同,必须解耦。

编辑器里需要大量辅助能力,例如选中态、高亮框、拖拽占位、组件边界、属性面板、撤销重做、页面草稿。这些能力不应该进入最终页面运行时。运行时只应该保留用户访问页面需要的能力:组件渲染、数据请求、埋点上报、降级容错和性能优化。

可以把整体链路理解成这样:

组件 NPM 包  -> 注册组件元信息  -> 编辑器读取组件协议  -> 用户拖拽生成页面 DSL  -> 发布系统保存页面 DSL  -> 运行时读取 DSL 并渲染页面

低代码平台真正沉淀的是 DSL 和组件协议,而不是编辑器本身。编辑器只是 DSL 的可视化生产工具。

页面 DSL 是平台的核心资产

用户拖拽出来的页面不能保存成 HTML,也不应该保存成 React 组件代码。更合适的是保存一份结构化 DSL。

一个页面可以抽象成组件树:

{  "pageId": "activity-001",  "title": "车型营销活动页",  "components": [    {      "id": "hero-1",      "type": "HeroBanner",      "props": {        "imageUrl": "https://cdn.example.com/banner.jpg",        "title": "新车上市"      }    },    {      "id": "carousel-1",      "type": "Carousel",      "props": {        "images": []      }    },    {      "id": "car-list-1",      "type": "CarList",      "props": {        "api": "/api/cars?series=123"      }    }  ]}

这份 DSL 至少要回答几个问题:

保存 DSL 的好处是可控。编辑器可以读取它继续编辑,运行时可以读取它渲染页面,发布系统可以对它做版本管理,回滚时也可以直接回到旧版本 DSL。

每个组件应该以 NPM 包独立开发

低代码平台是否能长期扩展,关键在组件体系。

如果所有组件都写在平台仓库里,短期开发快,但长期会出现几个问题:组件代码互相影响,版本无法独立发布,不同业务线难以并行开发,编辑器和组件运行时耦合越来越深。

更合理的方式是每个组件按 NPM 包独立开发:

@marketing-components/hero-banner@marketing-components/carousel@marketing-components/car-list@marketing-components/form-submit@marketing-components/coupon-card

每个组件包至少包含三部分:

src/index.tsx       组件运行时代码src/schema.ts       组件元信息和属性编辑协议src/preview.tsx     编辑器中的预览适配逻辑

组件包对外暴露统一结构:

export const componentMeta = {  type: "HeroBanner",  name: "头图组件",  category: "基础展示",  defaultProps: {    imageUrl: "",    title: "",    subtitle: ""  },  propsSchema: [    {      name: "imageUrl",      label: "图片地址",      type: "image",      required: true    },    {      name: "title",      label: "主标题",      type: "text",      defaultValue: ""    }  ]};export default HeroBanner;

编辑器不需要知道 HeroBanner 组件内部怎么写。它只读取 componentMeta,就能知道这个组件叫什么、属于哪个分类、默认属性是什么、右侧属性面板应该展示哪些表单项。

运行时也不需要知道编辑器如何配置属性。它只拿到 DSL 中的 typeprops,找到对应组件并渲染。

组件协议要同时服务编辑器和运行时

组件协议不能只描述表单字段。一个成熟的低代码组件协议至少要包含以下信息:

配置项作用
type组件唯一标识
name编辑器中展示的组件名称
category组件库分类
defaultProps拖入页面时的默认属性
propsSchema右侧属性面板如何渲染
dataSchema组件需要哪些异步数据
trackingSchema组件支持哪些埋点配置
version组件版本
runtime运行时加载入口

这样组件就具备了自描述能力。编辑器新增一个组件时,不需要手写右侧面板,也不需要硬编码组件分类。只要组件包发布并注册到平台,编辑器就能自动识别。

组件注册中心比组件代码更关键

组件包独立发布以后,还需要一个注册中心。否则编辑器不知道现在有哪些组件可用,运行时也不知道某个组件版本应该从哪里加载资源。

注册中心保存的不是组件源码,而是组件 manifest:

{  "type": "HeroBanner",  "version": "1.4.2",  "name": "头图组件",  "category": "基础展示",  "schemaUrl": "https://cdn.example.com/components/hero-banner/1.4.2/schema.json",  "runtime": {    "js": "https://cdn.example.com/components/hero-banner/1.4.2/index.js",    "css": "https://cdn.example.com/components/hero-banner/1.4.2/style.css"  },  "thumbnail": "https://cdn.example.com/components/hero-banner/1.4.2/thumb.png",  "compat": {    "runtime": ">=1.3.0"  }}

编辑器打开组件库时,读取的是这份 manifest 列表:

Editor  -> Component Registry  -> component manifest list  -> group by category  -> render material panel

运行时渲染页面时,也通过 manifest 找资源:

Page DSL: HeroBanner@1.4.2  -> Registry 查找资源地址  -> Runtime 加载 JS/CSS  -> Renderer 渲染组件

这样组件包、编辑器和运行时之间就不会互相硬编码。组件发新版时,只要发布新的 manifest,历史页面继续使用旧版本,新页面可以选择新版本。

这里要特别注意一个原则:组件资源不能覆盖同版本文件。HeroBanner@1.4.2 一旦发布,就应该是不可变的。否则历史页面今天打开和明天打开可能不是同一套组件逻辑,回滚也会失去意义。

组件包需要有清楚的构建产物

如果组件只是一个普通 React 组件包,编辑器和运行时很难稳定加载。低代码组件包最好规定构建产物:

dist/runtime.js     生产运行时入口dist/editor.js      编辑态扩展能力dist/schema.json    组件协议dist/style.css      组件样式dist/thumb.png      组件缩略图

其中 runtime.js 面向线上页面,要尽量轻;editor.js 可以包含编辑态辅助逻辑,例如占位展示、mock 数据、属性面板扩展、拖拽辅助。二者不要混成一个包。

组件构建后可以生成 manifest:

type ComponentManifest = {  type: string;  version: string;  schemaUrl: string;  runtime: {    js: string;    css?: string;  };  editor?: {    js: string;  };};

组件发布流程大概是:

pnpm build  -> 产出 runtime/editor/schema/style/thumb  -> 上传 CDN  -> 生成 manifest  -> 写入组件注册中心  -> 编辑器可见新版本

这条链路建立起来以后,组件团队可以独立发组件,平台团队只维护协议和加载机制。

Dev 模式要让组件开发脱离平台主仓库

组件独立成 NPM 包以后,开发体验必须跟上。否则每次改组件都要发布一个临时版本,再到平台里安装调试,效率会非常低。

Dev 模式需要解决两个问题:

组件开发者能在本地独立调试组件组件能挂载到编辑器里模拟真实低代码环境

一个比较实用的方式是提供组件开发脚手架:

pnpm dev  -> 启动组件本地 playground  -> 加载组件 schema  -> 提供 mock props 和 mock api  -> 展示组件在移动端页面中的效果

组件开发者可以在本地直接看到组件效果,也可以通过本地 registry 或 workspace link 接入编辑器:

组件包本地启动  -> 暴露组件入口和 schema  -> 编辑器 Dev 模式读取本地组件清单  -> iframe 预览区加载本地组件资源

Dev 模式下,编辑器最好支持切换组件源:

线上组件源:加载已发布组件版本本地组件源:加载开发中的组件包测试组件源:加载预发环境组件版本

这样组件研发、平台研发、业务配置可以相互解耦。组件不需要等平台发版,平台也不需要因为某个组件迭代频繁变得不稳定。

更进一步,Dev 模式应该支持本地 manifest 覆盖:

{  "type": "HeroBanner",  "version": "local",  "schemaUrl": "http://localhost:7101/schema.json",  "runtime": {    "js": "http://localhost:7101/runtime.js",    "css": "http://localhost:7101/style.css"  }}

编辑器启动时可以读取一个本地配置:

LOWCODE_COMPONENT_MANIFEST=http://localhost:7101/manifest.json

这样开发者改组件以后,iframe 预览区可以直接加载本地资源,甚至支持 HMR。调试链路就从“改组件、发包、平台安装、刷新页面”变成了“改组件、预览区即时更新”。

这个体验很重要。低代码平台如果组件开发体验差,最后所有人都会绕过平台,直接写定制页面。

iframe 让编辑器和运行时边界更清晰

中间预览区域使用 iframe 是一个重要设计。

如果直接在编辑器 DOM 中渲染活动页面,编辑器样式、事件、全局变量很容易污染页面运行时。iframe 可以提供隔离:

但 iframe 也带来通信问题。编辑器需要告诉 iframe:新增组件、删除组件、选中组件、更新属性、调整顺序。iframe 也需要告诉编辑器:当前点击了哪个组件、组件实际尺寸是多少、拖拽落点在哪里。

这意味着编辑器和 iframe 之间必须设计一套消息协议,而不是随意用 postMessage 传对象。

例如:

type EditorMessage =  | { type: "PAGE_SCHEMA_UPDATE"; payload: PageSchema }  | { type: "COMPONENT_SELECT"; payload: { id: string } }  | { type: "COMPONENT_PROPS_UPDATE"; payload: { id: string; props: object } };type PreviewMessage =  | { type: "COMPONENT_CLICK"; payload: { id: string } }  | { type: "DROP_POSITION_CHANGE"; payload: { index: number } }  | { type: "PREVIEW_READY" };

消息协议清晰以后,编辑器和预览运行时可以独立迭代。iframe 内部渲染实现变了,只要协议不变,编辑器就不用跟着改。

架构重点不是拖拽,而是可演进

很多人看低代码平台,会第一眼关注拖拽。但真正决定平台生命周期的是组件体系和协议设计。

拖拽只是编辑动作,组件协议才决定平台能支持多少组件;iframe 只是预览容器,运行时协议才决定页面能不能稳定发布;属性面板只是配置入口,schema 设计才决定业务能不能持续扩展。

一个可长期维护的低代码营销平台,至少要做到:

这些设计做好以后,低代码平台才不会变成一个越来越大的活动页仓库,而是能逐步沉淀成一套可扩展的页面生产系统。


Share this post on: