1. 项目概述:为什么“可扩展的活动落地页”不是前端工程师的KPI,而是增长团队的生存线
“Scale Campaign Landing Pages Without Tech Debt”——这个标题乍看像一句技术口号,实则直戳增长型业务最痛的软肋。我带过七支不同行业的增长团队,从SaaS工具到DTC电商,再到教育平台和本地生活服务,几乎每家都在第3~6个月遭遇同一个崩塌式现场:市场部连夜上线5个节日 campaign,每个都要独立落地页;设计给3套视觉稿;运营配5套UTM和埋点逻辑;而前端团队盯着Git冲突记录叹气:“这次改的是header组件,但A页用React 17,B页是Next.js 12,C页还跑在Vue 2上……合并PR前得先开个跨框架兼容会议。”这不是夸张,这是我在2022年Q3为一家跨境支付SaaS做增长审计时拍下的真实日志截图——那周他们因落地页发布延迟,错失了黑五预热黄金48小时,直接损失237万潜在线索。所谓“无技术债式扩展”,本质不是拒绝写代码,而是拒绝让每一次营销动作都变成一次小型系统重构。它解决的不是“能不能做”,而是“能不能在2小时内上线、2天内AB测试、2周内全量、且不惊动核心交易链路”。适合谁?CMO、增长负责人、营销技术(MarTech)负责人、全栈型增长工程师,以及那些被临时拉去“救火”的前端同学——尤其当你发现自己的component库里混着3种CSS-in-JS方案、4个状态管理模式、还有2个被遗忘的Web Component遗留模块时,这篇就是为你写的。
核心关键词“Scale”在这里不是指流量规模,而是指 并行campaign数量的线性增长能力 ;“Campaign Landing Pages”特指服务于短期营销目标(如产品发布、节日促销、白皮书下载、活动报名)的轻量级、高转化率单页,它们生命周期短(平均存活17天)、变体多(A/B/C/D多版本共存)、依赖强(需实时对接CRM、邮件平台、分析工具);而“Without Tech Debt”则明确划出红线:不接受通过复制粘贴模板、硬编码埋点、绕过CI/CD手动部署等方式换取短期上线速度。我见过太多团队用“先上线再说”换来三年无法升级React版本的技术枷锁——因为27个落地页里有19个依赖一个已归档的npm包,而那个包的作者早已离职。所以,这不是一篇讲“怎么写更好CSS”的文章,而是一份经过11个真实项目验证的、关于 如何把营销敏捷性嵌入工程DNA 的操作手册。
2. 整体架构设计:放弃“页面即应用”,拥抱“页面即配置”
2.1 为什么传统SPA模式在营销场景中天然失效
很多团队第一反应是:“我们用Next.js啊,SSG+ISR,性能好又现代。”这话没错,但错在混淆了“应用架构”和“营销内容架构”的根本差异。我拿自己操盘过的一个案例说明:某在线教育平台在暑期推广“Python入门训练营”,需要同步上线6个地域定制页(北京/上海/广州/深圳/杭州/成都),每个页需差异化展示本地讲师照片、线下教室地址、城市专属优惠券码,并实时显示“剩余名额”。若按标准Next.js SPA模式开发:
-
方案A:为每个城市建独立page路由(
/camp/beijing,/camp/shanghai…)。问题:6个页面=6套重复逻辑,CMS更新讲师信息要改6次;优惠券码生成逻辑分散在6个文件里,一旦风控策略调整,必须6处同步发版。 -
方案B:用动态路由
/camp/[city]+getStaticProps预生成。问题:城市列表硬编码在getStaticPaths里,新增城市需改代码、重新build;更致命的是,getStaticProps无法实时获取用户IP定位的城市,导致用户从上海访问/camp/beijing时,页面仍显示北京信息——而市场部要求“用户打开即见本地化内容”。
这两种方案都违背了营销落地页的核心诉求: 内容可配置、逻辑可复用、发布可原子化 。它们把“页面”当作不可分割的代码单元,而实际需求是把“页面”拆解为“结构(Layout)+ 内容块(Block)+ 数据源(Source)+ 行为规则(Rule)”四个正交维度。这正是我们放弃“页面即应用”转向“页面即配置”的底层动因。就像乐高,你不需要为每栋房子重造砖块,只需定义好砖块接口(尺寸、凸点位置、颜色编码),再按说明书拼装即可。我们的“砖块”是标准化的内容组件,“说明书”是JSON Schema驱动的页面配置,“拼装工”是轻量级渲染引擎。
2.2 四层解耦架构:Layout / Block / Source / Rule
我们最终落地的架构分四层,每层职责清晰、变更隔离,经受住了单月上线42个campaign的压测(2023年双十二期间数据):
第一层:Layout(布局层)—— 定义页面骨架与容器约束
Layout不是HTML模板,而是声明式JSON Schema。例如一个标准活动页Layout定义如下:
{
"id": "campaign-v2",
"name": "标准活动页布局",
"regions": [
{
"id": "hero",
"maxBlocks": 1,
"allowedBlockTypes": ["hero-banner", "video-hero"],
"required": true
},
{
"id": "features",
"maxBlocks": 5,
"allowedBlockTypes": ["feature-card", "comparison-table"],
"required": false
},
{
"id": "cta",
"maxBlocks": 1,
"allowedBlockTypes": ["email-form", "phone-cta", "countdown-timer"],
"required": true
}
]
}
关键设计点:
maxBlocks
和
allowedBlockTypes
构成强约束,确保设计师拖拽时不会把5个视频组件堆在首屏;
required
字段让CMS知道哪些区域必须填充,避免发布空页。我们只维护3个Layout:
campaign-v2
(标准活动)、
lead-gen-v1
(线索收集)、
event-reg-v1
(活动报名),覆盖92%的营销场景。新增Layout需走架构委员会评审,杜绝“每个campaign建一个Layout”的熵增。
第二层:Block(内容块层)—— 可复用、可配置的原子单元
Block是真正承载内容的单元,每个Block对应一个React组件(或Vue SFC),但
绝不包含业务逻辑
。以
email-form
Block为例,其组件代码仅处理UI渲染与表单事件,所有提交逻辑由Rule层注入:
// blocks/email-form.tsx
export const EmailFormBlock = ({
config, // 来自CMS的配置:title, placeholder, buttonLabel等
onSubmit // 由Rule层传入的函数,非硬编码
}) => {
const [email, setEmail] = useState('');
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit(email); }}>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={config.placeholder}
/>
<button type="submit">{config.buttonLabel}</button>
</form>
);
};
Block目录结构严格按类型划分:
blocks/
├── hero-banner/ # 首屏横幅(支持图片/视频/文字叠加)
├── feature-card/ # 特性卡片(图标+标题+描述)
├── countdown-timer/ # 倒计时(支持毫秒级精度,时区自动适配)
└── dynamic-pricing/ # 动态价格(根据UTM参数实时计算折扣)
每个Block必须提供
schema.json
定义其可配置字段(如
hero-banner
的
imageSrc
,
headline
,
ctaText
),CMS据此生成可视化编辑器。Block的复用率是我们考核前端团队的核心指标——2023年Q4,
countdown-timer
Block被37个campaign复用,
dynamic-pricing
被22个复用,而新开发Block仅5个,证明架构有效抑制了重复造轮子。
第三层:Source(数据源层)—— 解耦内容与呈现,统一数据契约
Source层解决“内容从哪来”的问题。我们强制所有外部数据必须通过Source Adapter接入,而非在Block内直接调用API。Adapter是薄胶水层,只做三件事:认证、请求、格式标准化。例如CRM Lead Source Adapter:
// sources/crm-lead.ts
export const crmLeadSource = {
id: 'crm-lead',
name: 'CRM线索数据源',
schema: z.object({
contactId: z.string(), // CRM中的联系人ID
utmParams: z.record(z.string()).optional() // UTM参数透传
}),
fetch: async (config) => {
const response = await fetch(`/api/crm/contacts/${config.contactId}`, {
headers: { 'X-API-Key': process.env.CRM_API_KEY! }
});
const data = await response.json();
// 标准化输出:所有Source必须返回此结构
return {
firstName: data.first_name || '',
email: data.email || '',
city: data.city || '未知城市',
customFields: data.custom_fields || {}
};
}
};
关键价值在于:当CRM从Salesforce切换到HubSpot时,只需重写
fetch
函数,所有使用该Source的Block(如
email-form
预填邮箱、
hero-banner
显示用户姓名)完全无需修改。我们目前维护7个Source:
crm-lead
,
product-catalog
,
inventory-status
,
geo-location
,
utm-params
,
ab-test-variant
,
realtime-availability
,覆盖全部营销数据需求。
第四层:Rule(行为规则层)—— 将业务逻辑从页面中剥离
Rule是架构的“大脑”,它定义“什么条件下触发什么行为”。Rule不写在页面代码里,而是以JSON Schema定义的DSL(领域特定语言)存储在CMS中,由Rule Engine在客户端运行。例如一个典型Rule:
{
"id": "apply-city-discount",
"trigger": "onPageLoad",
"condition": {
"source": "geo-location",
"field": "city",
"operator": "in",
"value": ["北京", "上海", "广州"]
},
"action": {
"type": "updateBlockConfig",
"blockId": "price-display",
"configPath": "discountRate",
"value": 0.15
}
}
这个Rule的意思是:“页面加载时,若用户定位城市在北京/上海/广州,则将
price-display
Block的折扣率设为15%”。Rule Engine会监听所有Source数据变更,并执行匹配的Rule。我们禁止在Rule中写复杂逻辑(如循环、递归),所有计算必须由Source Adapter预处理或Block自身完成。Rule的版本化管理让我们能回滚到任意历史状态——2023年某次大促,因Rule配置错误导致全站价格显示为0,我们30秒内切回前一版本,未影响订单。
2.3 架构演进路线图:从“静态模板”到“智能编排”的三阶段
没有团队能一步到位建成上述四层架构。我们为不同成熟度的团队设计了渐进式路径,每阶段交付可衡量的业务价值:
阶段一:静态模板中心(0~3个月)
目标:消灭复制粘贴,建立基础复用。
- 步骤1:梳理现有落地页,提取共性Layout(如80%页都有Header+Hero+Features+CTA+Footer),用HTML/CSS/JS编写3套纯静态模板,托管在CDN。
-
步骤2:为每个模板定义JSON Schema配置项(如
hero.title,features[0].icon),市场人员用Excel填写配置,脚本自动生成HTML文件。 - 步骤3:接入基础分析(Google Analytics)和表单提交(Webhook到CRM)。
- 关键成果:落地页上线时间从平均48小时降至4小时,模板复用率超70%。
提示:此阶段不碰任何框架,用最原始的字符串替换(如
{{hero.title}})实现,胜在快、稳、易理解。我曾帮一家本地家居品牌用此法,一周内将6个门店活动页全部上线,老板当场决定追加预算做阶段二。
阶段二:组件化引擎(3~6个月)
目标:实现Block级复用与动态数据绑定。
-
步骤1:将静态模板重构为React组件库,每个Block独立npm包(如
@company/hero-banner@1.2.0),语义化版本号。 - 步骤2:开发轻量级渲染器(<5KB),接收Layout ID + Block配置数组 + Source映射,动态挂载组件。
- 步骤3:接入Source层,支持从URL参数、Cookie、简单API获取数据。
- 关键成果:新增campaign开发成本下降65%,A/B测试变体发布速度提升3倍(只需改配置,不改代码)。
注意:此阶段必须建立严格的Block准入规范——所有新Block需通过“3分钟可配置”测试(非技术人员能否在5分钟内完成基础配置并预览),否则退回重做。
阶段三:智能编排平台(6~12个月)
目标:实现Rule驱动的个性化与自动化。
- 步骤1:开发Rule DSL编辑器(低代码界面),支持条件分支、数据映射、定时触发。
- 步骤2:构建Rule Engine SDK,支持Web、Email、SMS多端运行(同一Rule可在落地页和EDM中复用)。
- 步骤3:与CDP(客户数据平台)深度集成,支持基于用户画像的实时行为触发。
- 关键成果:个性化转化率提升22%(A/B测试数据),营销人员自主创建复杂规则占比达89%,工程师介入率降至12%。
实操心得:Rule DSL必须设计得足够“傻瓜”——我们曾用自然语言转Rule(如输入“如果用户来自北京且UTM_medium=wechat,显示红色按钮”),但发现准确率仅63%。最终采用“条件-动作”双面板拖拽,准确率100%,且市场人员培训2小时即可上岗。
3. 核心细节解析:Block开发、Source接入与Rule编排的实战要点
3.1 Block开发:如何写出“永不废弃”的内容组件
Block是架构的基石,其质量直接决定整个系统的寿命。我总结出Block开发的“三不原则”: 不耦合业务、不假设上下文、不处理副作用 。违反任一原则,该Block将在3个月内成为技术债黑洞。
不耦合业务
:Block只负责“怎么展示”,绝不决定“展示什么”。以
countdown-timer
为例,常见错误写法是:
// ❌ 错误:硬编码结束时间,耦合具体campaign
const endTime = new Date('2024-12-31T23:59:59');
正确做法是将其作为配置项暴露:
// ✅ 正确:所有业务参数外置
interface CountdownConfig {
endTime: string; // ISO 8601格式,如'2024-12-31T23:59:59'
timeZone?: string; // 如'Asia/Shanghai',默认浏览器时区
format?: 'days-hours-minutes' | 'hours-minutes-seconds';
}
这样,同一
countdown-timer
Block可服务于“黑五倒计时”、“课程开课倒计时”、“限时优惠倒计时”,只需改配置。我们要求每个Block的
props
接口必须通过
zod
校验,确保CMS传入的数据类型安全。
不假设上下文 :Block必须能在任意Layout、任意父容器中正常工作。这意味着:
-
禁止使用全局CSS类名(如
.btn-primary),所有样式必须scoped(CSS Modules或styled-components)。 -
禁止依赖父组件Context(如
AuthContext),所有依赖必须显式传入props。 -
禁止使用
document.getElementById等DOM操作,所有交互必须通过props回调(如onSubmit,onClose)。
我们曾发现一个
email-form
Block偷偷读取
window.location.search
解析UTM,导致在iframe嵌入场景下失效。修复方案是:将UTM参数作为
source
注入,Block只接收标准化后的
utmMedium
,
utmSource
字段。
不处理副作用
:Block内禁止发起网络请求、设置Cookie、调用localStorage。所有副作用必须由Rule或Source层处理。例如,表单提交成功后跳转到感谢页,不应写在
email-form
里:
// ❌ 错误:Block内处理跳转
if (response.success) {
window.location.href = '/thank-you';
}
而应通过
onSuccess
回调通知Rule层:
// ✅ 正确:解耦副作用
onSuccess && onSuccess({ email, utmParams });
然后由Rule定义“收到onSuccess事件后,跳转到/thank-you”。这样,同一表单可配置为“跳转感谢页”、“弹窗提示”、“播放音效”,甚至“发送Slack通知”,完全不改Block代码。
Block的性能守门员:懒加载与资源预判
营销落地页对首屏性能极度敏感。我们为每个Block强制添加
resourceHints
配置:
// blocks/video-hero.tsx
export const VideoHeroBlock = ({ config }) => {
// 预连接CDN域名,减少DNS查询时间
useEffect(() => {
const link = document.createElement('link');
link.rel = 'preconnect';
link.href = 'https://cdn.example.com';
document.head.appendChild(link);
}, []);
return (
<div>
{/* 视频资源预加载,但不自动播放 */}
<link rel="preload" as="video" href={config.videoSrc} />
<video src={config.videoSrc} muted playsInline />
</div>
);
};
更进一步,我们开发了
ResourceHintInjector
工具,在构建时扫描所有Block的
config
字段,自动注入
<link rel="preload">
和
<link rel="prefetch">
标签。实测下来,视频首帧时间缩短42%,LCP(最大内容绘制)指标从4.2s降至1.8s。
3.2 Source接入:如何让CRM、CDP、库存系统“乖乖交出数据”
Source层是架构的“数据海关”,其设计质量决定系统能否应对复杂的营销数据流。我们坚持“一个Source,一个责任”的铁律——绝不允许一个Source Adapter同时处理CRM数据和库存数据。
Source的契约设计:标准化输入与输出
每个Source必须明确定义其输入(Input Schema)和输出(Output Schema)。以
inventory-status
Source为例:
// sources/inventory-status.ts
export const inventoryStatusSource = {
id: 'inventory-status',
name: '库存状态数据源',
// 输入:必须提供productSku(商品SKU)和warehouseId(仓库ID)
inputSchema: z.object({
productSku: z.string(),
warehouseId: z.string().optional() // 可选,不填则查总仓
}),
// 输出:必须返回标准化的库存对象
outputSchema: z.object({
inStock: z.boolean(), // 是否有货
quantity: z.number().min(0), // 可售数量
restockDate: z.string().nullable(), // 补货日期(ISO格式)
isLowStock: z.boolean() // 是否低库存(<10件)
}),
fetch: async (input) => {
const url = input.warehouseId
? `/api/inventory/${input.productSku}?warehouse=${input.warehouseId}`
: `/api/inventory/${input.productSku}`;
const res = await fetch(url);
const data = await res.json();
return {
inStock: data.quantity > 0,
quantity: data.quantity,
restockDate: data.restock_date || null,
isLowStock: data.quantity < 10
};
}
};
关键设计点:
inputSchema
和
outputSchema
用Zod定义,构建时自动生成TypeScript类型和CMS表单。当库存API返回字段变更(如
quantity
改为
available_quantity
),只需更新
outputSchema
和
fetch
函数,所有消费该Source的Block自动获得新字段,无需修改。
Source的熔断与降级:当第三方系统宕机时,你的落地页不能白屏
营销活动期间,CRM或CDP宕机是常态。我们为每个Source强制实现熔断(Circuit Breaker)和降级(Fallback):
// utils/circuit-breaker.ts
export class CircuitBreaker<T> {
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private failureCount = 0;
private readonly failureThreshold = 3;
private readonly timeoutMs = 30000; // 30秒
async execute(fetchFn: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
// OPEN状态下直接返回fallback
return this.fallback();
}
try {
const result = await Promise.race([
fetchFn(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), this.timeoutMs)
)
]);
this.failureCount = 0;
this.state = 'CLOSED';
return result;
} catch (error) {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
console.warn(`Circuit breaker opened for ${this.id}`);
}
throw error;
}
}
private fallback(): T {
// 返回安全的默认值,如库存默认为"有货"
if (this.id === 'inventory-status') {
return {
inStock: true,
quantity: 999,
restockDate: null,
isLowStock: false
} as any;
}
return {} as any;
}
}
当
inventory-status
连续3次调用失败,熔断器进入OPEN状态,后续请求直接返回
{inStock: true}
,保证页面正常渲染。10分钟后自动进入HALF_OPEN状态,尝试一次请求,成功则恢复CLOSED。这套机制让我们在2023年某次Salesforce大面积故障中,所有落地页库存显示“有货”,用户可正常下单,故障恢复后数据自动同步,零客诉。
Source的调试神器:Source Inspector面板
为加速问题排查,我们在开发环境注入
SourceInspector
面板(Ctrl+Shift+S呼出),实时显示所有Source的状态:
-
当前输入参数(如
{productSku: 'PROD-123', warehouseId: 'WH-BJ'}) - 最近一次响应(含HTTP状态码、耗时、原始JSON)
- 熔断器状态(CLOSED/OPEN/HALF_OPEN)
- 缓存命中率(我们为Source启用LRU缓存,默认5分钟)
当市场人员反馈“北京仓库库存没更新”,工程师打开面板,一眼看到
inventory-status
的响应是
{inStock: false, quantity: 0}
,而CRM后台显示有货——问题立刻定位到Source Adapter的
warehouseId
参数拼写错误(传了
warehouse_id
而非
warehouseId
),5分钟修复。
3.3 Rule编排:用“条件-动作”DSL替代手写JavaScript
Rule层是营销人员与技术的交汇点,其设计必须平衡表达力与安全性。我们彻底放弃让用户写JavaScript(哪怕只是
if/else
),转而设计一套安全、可审计、可版本化的DSL。
Rule的语法设计:为什么不用YAML或JSON Schema?
早期我们尝试用JSON Schema定义Rule,但市场人员抱怨“写JSON太容易少逗号”。后来改用YAML,又出现缩进混乱问题。最终我们选择自研极简DSL,语法仅3条规则:
# 规则ID(唯一标识)
rule: apply-early-bird-discount
# 触发时机:onPageLoad | onUserAction | onTimer | onSourceUpdate
when: onPageLoad
# 执行条件(支持嵌套AND/OR)
if:
- source: utm-params
field: utm_campaign
operator: equals
value: "early-bird-2024"
- source: geo-location
field: country
operator: equals
value: "China"
# 执行动作(支持链式调用)
then:
- action: updateBlockConfig
blockId: price-display
configPath: discountRate
value: 0.2
- action: showBlock
blockId: early-bird-badge
这套DSL的优势:
- 零学习成本 :市场人员说“如果UTM是early-bird且用户在中国,就打8折并显示徽章”,直接照抄成DSL。
-
语法安全
:Parser在保存前校验所有字段,缺失
rule:或then:直接报错,不入库。 - 可追溯 :每条Rule保存时自动记录创建人、时间、变更摘要(如“将discountRate从0.15改为0.2”)。
Rule的执行引擎:如何在毫秒内完成条件匹配
Rule Engine不是解释器,而是编译器。当Rule保存时,引擎将其编译为高效JavaScript函数:
// 编译前DSL
if:
- source: utm-params
field: utm_medium
operator: equals
value: "email"
- source: ab-test-variant
field: variant
operator: equals
value: "B"
// 编译后JS函数
function rule_12345(data) {
return (
data['utm-params']?.utm_medium === 'email' &&
data['ab-test-variant']?.variant === 'B'
);
}
引擎维护一个Rule索引表,按
source
字段哈希分组。当
utm-params
数据更新时,只触发订阅了该Source的Rule,避免全量遍历。实测在200条Rule并发场景下,单次匹配耗时稳定在0.8ms以内,对页面性能无感知。
Rule的灰度发布:如何让新规则“悄悄上线”
新Rule上线前必须灰度。我们设计了三层灰度:
-
用户层灰度
:按用户ID哈希,10%用户生效(如
userId % 100 < 10)。 - 设备层灰度 :仅iOS设备生效(用于测试Safari兼容性)。
- 地理层灰度 :仅广东省用户生效(用于区域活动试点)。
灰度配置与Rule本身分离,存于独立配置项。发布时,运维只需修改灰度比例,无需动Rule代码。2023年Q4,我们用此机制将一条涉及价格计算的Rule先在0.1%用户中运行,发现小概率下
restockDate
为空导致NaN错误,修复后全量,零事故。
4. 实操过程:从零搭建可扩展落地页系统的完整步骤
4.1 环境准备与工具链选型:为什么我们放弃Webpack,选择Vite
搭建可扩展落地页系统,第一步不是写代码,而是选对工具链。我们曾用Webpack构建,但随着Block数量增长,HMR(热模块替换)越来越慢,开发体验急剧下降。2022年Q2,我们全面迁移到Vite,原因如下:
Vite的原生ESM优势
:营销落地页的Block本质是大量独立组件,Vite的按需编译(On-Demand Compilation)让每个Block的启动时间从Webpack的1200ms降至180ms。当我们新增一个
dynamic-pricing
Block时,Vite只编译该文件及其依赖,而Webpack需重建整个chunk。
Vite插件生态的精准打击 :我们定制了3个核心插件:
-
vite-plugin-block-scan:扫描blocks/目录,自动生成Block注册表(blockRegistry.ts),供渲染器动态导入。 -
vite-plugin-source-injector:在构建时注入Source Adapter的类型定义,确保source字段在TSX中可智能提示。 -
vite-plugin-rule-compiler:将DSL文件(.rule)编译为JS函数,输出到dist/rules/。
Vite配置精简到极致:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import blockScan from './plugins/vite-plugin-block-scan';
import sourceInjector from './plugins/vite-plugin-source-injector';
import ruleCompiler from './plugins/vite-plugin-rule-compiler';
export default defineConfig({
plugins: [react(), blockScan(), sourceInjector(), ruleCompiler()],
build: {
rollupOptions: {
external: ['react', 'react-dom'], // Block不打包React,由渲染器提供
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM'
}
}
}
}
});
为什么不用Next.js或Remix?
Next.js的App Router虽强大,但其
getServerSideProps
和
getStaticProps
与我们的“页面即配置”理念冲突——我们不需要服务端渲染整页,只需要在客户端按需加载Block。Remix的嵌套路由也过度复杂。Vite的轻量与灵活,让我们能完全掌控渲染流程,这才是营销场景需要的。
4.2 第一个可运行的落地页:5分钟完成“Hello World”级Demo
不要被四层架构吓到。下面带你5分钟跑通最小可行系统(MVP),验证核心链路是否通畅。
步骤1:初始化项目(2分钟)
npm create vite@latest landing-page-engine -- --template react
cd landing-page-engine
npm install
# 安装核心依赖
npm install zod @tanstack/react-query
# 创建目录结构
mkdir -p src/{blocks,sources,rules,engine}
步骤2:编写第一个Block(1分钟)
src/blocks/hello-world/index.tsx
:
export const HelloWorldBlock = ({ config }: { config: { message: string } }) => {
return <h1>{config.message || 'Hello, World!'}</h1>;
};
// Block Schema,供CMS生成表单
export const helloWorldSchema = {
message: { type: 'string', label: '欢迎消息', defaultValue: 'Hello, World!' }
};
步骤3:编写第一个Source(30秒)
src/sources/mock-data.ts
:
export const mockDataSource = {
id: 'mock-data',
name: '模拟数据源',
inputSchema: {} as const,
outputSchema: {} as const,
fetch: async () => ({})
};
步骤4:编写第一个Rule(30秒)
src/rules/hello.rule
:
rule: hello-world-rule
when: onPageLoad
then:
- action: updateBlockConfig
blockId: hello-world
configPath: message
value: "Welcome to Campaign Engine!"
步骤5:创建渲染入口(1分钟)
src/main.tsx
:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { HelloWorldBlock } from './blocks/hello-world';
import { mockDataSource } from './sources/mock-data';
import { compileRule } from './engine/rule-compiler';
// 模拟CMS传入的页面配置
const pageConfig = {
layoutId: 'campaign-v2',
blocks: [
{ id: 'hello-world', type: 'hello-world', config: {} }
],
sources: [{ id: 'mock-data', config: {} }],
rules: ['hello.rule']
};
// 渲染器核心逻辑(简化版)
const renderPage = () => {
const root = ReactDOM.createRoot(
document.getElementById('root')!
);
root.render(
<React.StrictMode>
<HelloWorldBlock config={{ message: 'Welcome to Campaign Engine!' }} />
</React.StrictMode>
);
};
renderPage();
运行
npm run dev
,打开
http://localhost:5173
,看到“Welcome to Campaign Engine!”即成功。这5分钟验证了:Block可独立开发、Source可接入、Rule可编译、渲染器可工作。接下来,你就可以按需扩展——加一个
email-form
Block,接一个
crm-lead
Source,写一条
pre-fill-email
Rule,整个系统就活了。
4.3 CMS集成:如何让市场人员“所见即所得”地配置页面
CMS不是技术负担,而是架构的放大器。我们不自研CMS,而是基于Strapi(开源Headless CMS)二次开发,因其插件生态成熟、API友好、且可私有化部署。
CMS的三张核心内容类型(Content Types)
在Strapi中,我们只定义3种Collection Type:
-
Pages(页面)
-
字段:
title(页面标题)、slug(URL路径)、layout(关联Layout)、blocks(动态区,关联Block)、sources(关联Source)、rules(关联Rule) -
关键设计:
blocks字段使用Dynamic Zones,允许拖拽排序;sources和rules使用Relation字段,支持多选。
-
字段:
-
Blocks(内容块)
-
字段:
type(枚举:hero-banner, email-form...)、config(JSON字段,存储Block配置) -
关键设计:
config字段在Admin Panel中渲染为JSON Schema表单,根据type动态加载对应Schema。
-
字段:
-
Rules(规则)
-
字段:
dsl(Text字段,存储DSL代码)、status(Draft/Published)、grayScale(灰度配置) -
关键设计:
dsl字段旁有“语法检查”按钮,点击即调用Rule Compiler API,返回错误位置。
-
字段:
CMS的“一键发布”工作流
市场人员配置完页面后,点击“发布”按钮,触发以下自动化流程:
-
CMS调用
/api/build接口,传入Page ID; -
构建服务拉取最新代码,执行
vite build --mode production; -
构建产物(HTML+JS+CSS)上传至CDN,生成唯一URL(如
https://cdn.example.com/pages/12345/index.html); -
CMS更新Page记录的
publishedUrl字段; - DNS自动CNAME到该URL(通过Cloudflare API)。
整个
1431

被折叠的 条评论
为什么被折叠?



