摘要:公司运营平台早期每上一个活动页都要走"提需求→开发→联调→提测→发布"的完整流程,简单活动也要3天起步。我们把重复出现的页面结构抽象为JSON Schema,把高频业务组件做成可视化卡片,运营在后台拖拽配置即可生成页面,开发从"写页面"变成"维护引擎和组件库"。上线后常规活动页搭建耗时降到4小时以内,这套引擎后来被3个项目直接复用。本文复盘从"发现重复模式"到"抽象配置协议"再到"落地可视化编辑"的完整思路,以及配置化方案被反复复用背后那几个关键的设计决策。
一、问题起点:为什么每个活动页都像"重新发明一遍"
接手运营平台时,我先做了一件不太"工程师"的事:把过去半年上线的30多个活动页代码翻了一遍,找共性。结论让我有点意外——这些页面看起来五花八门,拆开看结构高度雷同:
- 90%的页面是"头部Banner + N个内容区块 + 底部规则说明"的纵向排列;
- 内容区块的类型就那么几种:商品列表、优惠券卡片、图文楼层、倒计时、跳转入口、Tab切换;
- 每次活动的差异主要在:文案、图片、商品ID、优惠券ID、颜色、排序、生效时间;
- 真正需要"新开发"的场景(全新交互形态)半年里只出现了3次。
也就是说,我们一直在用"写代码"的方式解决"填内容"的问题。每个活动页的开发工时里,70%花在重复搭结构、调样式、对接接口上,只有30%是真正的新逻辑。3天的排期里,大部分时间是在做可以不做的事。
这个观察决定了方案方向:不是做一个更强的脚手架,而是把"页面结构"从代码里抽出来,变成数据。
二、核心抽象:页面 = JSON Schema + 组件注册表
整个引擎的地基只有两个概念:
┌─────────────────────────────────────┐ |
一份典型的页面Schema长这样:
{ |
运行时渲染引擎做的事情非常朴素:遍历 blocks,按 type 从注册表取组件,把 props 传进去。
function PageRenderer({ schema }: { schema: PageSchema }) { |
两个容易被低估的设计点:
- 每个区块包一层ErrorBoundary:配置是人填的,填错一个字段不应该让整个页面白屏。单区块渲染失败降级为占位符,其余区块正常展示。上线一年多,这个机制兜住了几十次配置事故。
- 注册表对引擎透明:渲染引擎永远不知道有"banner"还是"coupon-list",它只认
registry.get(type)。这是后来跨项目复用的关键——换项目就是换注册表,引擎一行不改。
三、组件卡片化:把高频区块做成"运营能看懂"的积木
Schema解决了"机器怎么渲染",但运营不可能手写JSON。
所以我们把每种区块类型封装成一张可视化卡片,卡片 = 组件实现 + 配置表单定义 + 实时预览:
// 卡片注册示例 |
卡片化的核心价值在于把配置权下放到运营手里的同时,把风险关进笼子:
- 运营只能配置卡片声明过的字段,不可能"顺手"改坏其他部分;
widget类型决定了输入方式:优惠券用选择器(从券系统拉真实数据,杜绝手填错ID)、图片用素材库(强制尺寸校验)、跳转链接用页面选择器(杜绝死链);- 每张卡片自带预览,改一个字段立刻看到效果,所见即所得是配置化能不能真正落地的分水岭——没有预览,运营还是得"配完发布再看",效率提升会大打折扣。
首批我们沉淀了12种卡片(Banner、优惠券、商品货架、图文楼层、倒计时、Tab容器、跳转入口、弹窗、抽奖、直播入口、规则说明、空白间距),覆盖了历史30个活动页中95%以上的区块需求。
四、可视化编辑器:拖拽只是表象,数据流才是内核
编辑器的交互(左侧卡片库、中间画布拖拽排序、右侧属性面板)业界方案很多,不展开。真正花心思的是这几个数据层设计:
4.1 编辑态与运行态共用一套Schema
编辑器内部状态就是Page Schema本身,拖拽、改属性、删区块都是对Schema的不可变更新(immer),撤销/重做就是Schema快照栈。
编辑器没有自己的私有数据结构,这保证了"编辑器里看到的"和"线上渲染的"永远是同一份数据,杜绝了两套模型不一致产生的灵异Bug。
4.2 草稿、版本与灰度发布
- 草稿箱:编辑中的Schema存草稿表,运营可以随时保存退出,不影响线上;
- 版本快照:每次发布生成不可变版本号,线上页面按版本渲染。出问题一键回滚到上一版本,10秒生效;
- 定时生效:活动的
startTime/endTime由引擎在渲染层判断,运营提前一周配好中秋活动,到点自动切换,不需要人肉半夜发布。
4.3 发布前校验:把事故拦截在编辑器里
发布动作触发一组自动检查,全部通过才允许上线:
✓ Schema结构校验(JSON Schema验证) |
其中"资源有效性"这条拦截率最高——运营经常配了已过期的券或已下架的商品,以前这类问题要到用户投诉才暴露,现在发布时就报出来。
五、性能与体验:配置化页面的代价怎么还
动态渲染相比硬编码页面有几个天然劣势,我们逐一处理:
- 首屏渲染:Schema + 组件JS是运行时组装的,包体积容易失控。解法:区块组件按需加载(
React.lazy+ 路由级预取),Schema里声明了哪些type,只加载对应chunk; - 接口瀑布:多个区块各自请求数据会形成串行瀑布。解法:渲染引擎扫描Schema收集所有
source.type === 'api'的区块,BFF层聚合为一个批量接口,一次请求返回全部区块数据; - C端缓存:活动页读多写少,Schema和聚合数据都做CDN缓存 + 版本戳,发布时主动刷新。页面秒开率从82%提到96%;
- 降级兜底:Schema拉取失败时展示本地缓存的上一版本;组件加载失败时展示占位图。活动页是大促流量入口,宁可展示旧内容,不能白屏。
六、复用实录:为什么这套引擎能被3个项目"抄作业"
引擎稳定后,先后被另外3个项目复用:
| 项目 | 复用内容 | 适配工作量 |
|---|---|---|
| 会员权益中心 | 引擎 + 8张通用卡片,新增4张权益类卡片 | 3人日 |
| 设备配网引导页 | 引擎 + 基础卡片,新增步骤引导容器 | 2人日 |
| 商城频道页改版 | 引擎 + 全量卡片,仅换主题token | 1人日 |
复盘下来,能被复用不是因为"代码写得通用",而是三个刻意为之的设计决策:
① 抽象层次卡在"页面区块"这个业务概念上。 往上抽象成"万能低代码平台"会陷入属性面板无限膨胀的泥潭;往下沉成"UI组件库"又失去了配置驱动的意义。"区块"恰好是运营心智模型和技术组件模型的交点——运营说的"加个优惠券楼层",就是Schema里的一个block。
② 引擎、卡片、数据三层严格分离。 引擎只依赖Schema协议,卡片只依赖注册接口,数据获取只依赖source声明。任何一层替换都不波及其他层。换项目 = 换卡片注册表 + 换数据源配置,引擎原封不动。
③ 配置协议自带版本演进机制。 Schema有 protocolVersion 字段,引擎对旧版本协议做兼容转换。这意味着各项目可以独立升级,不会被引擎版本锁死。
一句话总结:复用的前提是边界清晰,边界清晰的前提是抽象克制。
七、踩坑与反思:配置化不是银弹
- 卡片数量要克制。中期一度膨胀到30+种卡片,运营面对选择困难,很多卡片半年没人用。后来做了使用统计,下线了9种低频卡片,合并了4种相似卡片,收敛到21种。**卡片库的熵增速度比你想象的快,要定期"断舍离"**。
- 别让Schema变成编程语言。有同事提议在
visibleWhen里支持任意JS表达式,被我否决了。声明式的条件(字段+操作符+值)能覆盖95%场景,剩下5%通过扩展新卡片类型解决。一旦Schema支持图灵完备的逻辑,调试、校验、安全都会失控。 - 预览环境必须与生产同源。早期预览用的是独立mock环境,出现过"预览正常、线上样式错乱"的事故。后来预览直接复用生产渲染引擎 + 生产组件构建产物,只是数据走mock。预览的可信度决定了运营敢不敢自己发布。
- 权限和审计不能省。谁能改哪个活动、谁发布了什么版本、改了什么字段,全部留痕。配置化把发布权下放给了非技术人员,相应的责任追溯体系必须跟上,否则出事故时说不清。
- 给开发留"逃生舱"。极少数全新交互(如那半年3次的新形态),通过"自定义HTML区块卡片"(受限的iframe沙箱)承接,开发写完嵌入,不阻塞引擎迭代。配置化系统必须承认自己覆盖不了100%,为剩下的部分留好出口,才不会逼着大家绕开系统。
八、写在最后
回头看,这个项目最值钱的产出不是那套引擎代码,而是两个认知:
第一,做配置化之前先做"重复模式考古"。 翻30个历史页面找共性这件事,比任何架构设计都重要。没有这个观察,你抽象出来的Schema大概率是错的——要么太薄(覆盖不了真实需求),要么太厚(为了想象中的灵活性过度设计)。
第二,配置化的终极目标是转移决策权,而不是消灭代码。 引擎上线后,前端并没有变闲:卡片库要迭代、渲染性能要优化、校验规则要完善。变化的是工作的性质——从"响应每个活动需求"变成"维护一个让运营自助的平台"。3天到4小时的差距,本质上是把70%的重复劳动还给了机器和流程。
如果你的业务里也存在"结构雷同、内容常变"的页面群,希望这篇复盘能给你一些参考。抽象要克制、边界要清晰、预览要可信、审计要完整——想清楚这四件事,配置化才能真正产生复利。
如果这篇对你有帮助,欢迎收藏网址。后续会继续分享运营类平台治理系列的其他实践(SSE流式取数引擎、可配置卡片选择系统等),关注不迷路。