星创编辑器在投放业务中的落地|得物技术

搭建一个落地页需要涉及到多方合作,需要不断地进行沟通协调。繁杂的流程需要耗费很多的时间,因此我们推动产品重新搭建了一个专门服务于软广投放流程的编辑器——星创,完成广告搭建在投放业务各系统中的闭环。

一、落地页技术架构

名词解释

模板:就是投放业务人员配置一个完整的落地页。

站外落地页:投放在媒体,站外的用户看到的落地页。

站内落地页:用户点击站外落地页,进入App站内看到的H5页面。

编辑器:是一个提供投放业务人员的搭建平台。由(画布+设置器)+生成器组成,搭建的数据源是运营常用落地页模板,模板的组成又分为基础模板、玩法模板、定制化模板,实体间遵循的通信协议是DSL。

技术架构图

落地页的大体功能可以参考下面整体的架构图。

整体的架构包含应用层、B端编辑器SDK、C端Node渲染层、以及基于nest.js数据服务存储层。

图片

下面我将从基础框架、模版类型配置、模版渲染3个方面阐述落地页编辑器的技术选型思路。

基础框架搭建

我们是基于pnpm+monorepo+turbo重新搭建的一个新项目。因为我们涉及到3个端的应用,有SSR、nest.js后端服务、前端H5 editor编辑器应用。整体之前肯定会存在一些代码共用,通用的逻辑、通用的utils。如下面图所示。

图片

再说这个架构的好处,就是在开发环境下,如果需要新增一个外投组件,如果不是这种架构,是普通的多项目架构的话,本地调试需要启动两个项目,这是bad case。再monorepo的架构下,只需要在compoents新增一个物料,然后node和B端H5复用这个物料代码,从components去引用这个组件就可以了。在开发环境调试的时候,只需要启动editor这个H5项目,而不是需要启动,H5项目和SSR项目,提高了开发体验。还有就是复用公共的基建,公共的配置、公共的打包。

二、模板JSON设计

这里说的模板,就是投放业务人员配置一个完整的落地页。

编辑器的低代码协议还是以JSON为主,主要设计了模板类型,模板基础配置,模板可变配置、模板通用能力配置。

如下面所示

{// 模板对应的类型templateType: string, // 模板变化的配置defaultConfig: {}, // 模板可变配置variableConfig:{}// 模板不变的配置globalConfig: {},   
}

这里这么设计的考虑有下面👇🏻几点:

新增模板类型

关于为什么新增模板类型,我给出了下面关于投放业务的思考。

  • 是为了渲染层对于不同模板类型走不同的渲染模式。比如静态化的广告落地页,不涉及请求接口的。统一走SSG或者ISR的渲染模式。如果是依赖接口请求的落地页,走SSR或者是react 18的流式渲染RSC。

因为我们需要支持多种渲染模式,所以在一开始做SSR技术选型的时候,优先选择了next 14版本,可以后面更好地支持业务。

  • 不同的渲染模式,打包出来的产物不同,性能优化的策略侧重点也是不一样的。

  • 之前投放地落地页链接都是xx-plus/xxx/xx,没法一眼看出当前落地页属于哪个类型,由于我们需要做精细化的落地页数据归因,所以我们存储了落地页的模板类型,所以最后投放链接都是这样。

https://cdn-xx.xx.com/xx-plus/{{模板类型}}/{{模板ID}}
  • 批量修改场景:过去投放落地页会有在节假日进行批量换图、批量修改配置的需求。过去在哪吒中中需要投入大量的维护成本,我们将落地页的类型维度单独抽象出来,通过支持对类型的批量修改来进一步提效。

抽象模板配置

globalConfig:主要是落地页的通用配置,是一个保留字段,主要是对落地页某个类型,同样添加某些功能,比如自动换端、自动全屏点击、一些投放策略的优化、 页面配置信息……

defaultConfig:主要是模板的基础配置,就是运营新建一个模板,这里配置已经不需要填了,已经由框架侧内置,进一步节省运营的配置时间。

variableConfig:主要是模板的可变配置,也就是运营经常会配置的。

我这里上一张图方便大家理解下:

图片

如上图所示:

之前落地页都通过内部的A搭建器进行搭建。A搭建器为了保持通用性,提供大量配置,从实践上看,配置的数量和复杂性,对投放运营来说是非常大的心智负担。因此在设计星创编辑器时,我们将符合业务特点的经验配置直接做了内置,并且简化了大量配置。投放业务人员只需要做一些简单的配置,就可以完成落地页的搭建。

图片

我举其中一个例子我们来看下对比,配置商品流落地页简化到了只需要选择自己的商品ID就已经完成了站内站外落地页搭建。

图片

我们已经内置了站内落地页,创建一个站外落地页,会自动把站内落地页同时创建,然后塞到站外落地页的还原链接里。

图片

模板渲染

实际渲染的数据是defaultConfig和variableConfig进行对象深度合并,形成最终落地页所SSR渲染需要的数据。对于落地页模板的通用能力,比如星创落地页需要支持自动换端,那么这个配置显然这是落地页的通用能力,对于这个配置我们是放在globalConfig中,开启了表示落地页支持自动换端。

我这里对容器的定义,就是根据模板类型,选择不同的渲染容器。目前支持的容器,有静态容器,和动态容器。

静态容器,顾名思义也就是在构建的时候,或者是根据接口数据能够知道当然渲染的模板数据。但是动态容器也就是没法在SSR侧知道渲染哪一个组件,比如我们的AB外投测试落地页,至于最终渲染哪个落地页需要在客户端才知道当前用户命中的是哪一个组件。

我整理了一些流程图方便大家理解:

图片

三、后台权限设计

编辑器的node服务主要有4大块,投放业务人员搭建、内部接口http调用,SSR渲染调用,对应不同的网关。

如下图所示我在nest基础上了,用了gurad路由守卫,自定义user装饰器,为了方便获取用户信息,和打关键用户行为日志。

图片

四、B端编辑器的选型思考

关于B端编辑器主要有2块,第一个是B端的画布、第二个是BC端组件配置如何映射,第三个编辑器的工具栏。

画布配置预览

这里由于我们的业务是外投的落地页C端页面,主要是移动端H5。所以整体的画布功能和PC端画布功能是取舍的。H5不需要特别复杂的功能,比如拖拽、样式、resize……唯一的要求做到所见就所得的就好了。

当时做画布配置预览的时候主要考虑了两种技术方案,一种是ifrmae去做整个编辑器,配置预览。第二种是组件、配置支持动态化。以远程组件的方式进行加载。我下面分别分析一下2种方案的优缺点。

动态远程组件

整体的流程大概分这几步:

  1. 先有一个组件。

  2. 将组件打包成UMD格式,可供浏览器使用(后面会介绍UMD),或者使用System.js去打包。

  3. 将其上传到CDN,然后还需要一个物料管理平台,主要管理组件的版本、支持注册、可回滚。

  4. 编辑器沙箱能力的考虑,防止污染全局。

  5. SSR根据组件类型进行动态渲染。

伪代码如下

const DynamicComponent = ({name, children, ...props}) => {const Component = useMemo(() => {return React.lazy(async () => fetchComponent(name))}, [name])return (<Suspensefallback={<div style={{alignItems: 'center', justifyContent: 'center', flex: 1}}><span style={{fontSize: 50}}>Loading...</span></div>}><Component {...props}>{children}</Component></Suspense>)
}export default React.memo(DynamicComponent)

这里使用到了React中的Suspense组件和React.lazy方法,关于他们的用法这里不做过多解释,整个DynamicComponent组件的含义是远程加载目标组件,这个过程该组件会渲染传入Suspense参数fallback之中的内容,最后会使用加载成功的组件进行替换。

优点:

  • 动态性(当组件需要更新时,可以直接JS内容,就可以实现动态更新)。

  • 不确定性(对于主应用而言,其不知道用户会拖拽多少个组件以及每个组件长什么样,它只需要将用户拖拽的JSON数组进行循环遍历,并渲染,然后将配置的属性传递过去就可以了,具体到每个组件具体是长什么样其不关心)。

劣势:

  • 我们一个落地页的组件类型的是确定的,在编辑器配置落地页,只有选择模板类型,选择完模板类型,自动会加载模板对应的组件配置。不存在用户拖拽、选择。星创落地页产品的核心逻辑就是轻量。

Iframe

Iframe也就是中间预览的画布是SSR提高一个预览url。

只需要在node层对每个落地页类型,都新增一个预览preview路由和正式投放路由做区分。这么做的好处,预览路由和正式路由,在逻辑上解耦。可以做一些在node做一些定制化的业务预览逻辑。比如飞书审核的时候,设计看到的就是预览路由,不是真正的路由。审核没有通过的落地页,正式线上投放链接,看到是带有水印的。

目录结构如下图所示:

图片

渲染的伪代码如下:

///预览路由
export default HocPreview(({ componentList }) => <Container componentList={componentList} />)// 正式路由
export default HocApp(({ componentList }: { componentList: any }) => {return <Container componentList={componentList} />
})

这里我是通过两个React HOC组件去做业务逻辑分离的处理。

好处:

  • 天然的沙盒化因为iframe的天然隔离性,画布渲染器中的所有逻辑、样式不会影响编辑器本身。

  • 利于多人编辑单人编辑时使用iframe进行通信,而多人编辑时可将iframe通信切换成WebSocket通信,设计时有异曲同工之妙。

  • 编辑器页面打开快,体验好,不需要加载很多物料,预览的加载收敛到SSR去处理。

坏处:

  • 可扩性特别差,新增落地页类型,都需要开发、测试。不支持定制化。

  • 没法做到组件级别的回滚。

最终左右对比,从项目开发周期、上线时间、业务模型的综合考虑下,我还是选择了iframe作为画布预览方案。

画布通信

说画布通信前,我先说下编辑器全局数据流的技术选型。Hooks时代的react状态管理库,已经不是臃肿的redux,我们应该全面拥抱hooks,所以也就考虑了两种比较有代表性的状态管理库,Zustand和Valito。

Valtio是围绕ES6的Proxy特性来进行设计的,它有以下几点核心的特性:

  • 代理状态,基于Proxy。

  • 响应式更新,对状态的操作都会通过代理进行,Valtio内部会跟踪并自动渲染。

  • 细粒度渲染,Valtio的代理可以实现细粒度的依赖跟踪,这样只有组件实际使用的状态变化时才会重新渲染,避免了不必要的渲染,性能方面很不错。

  • 简洁的API,Valtio尽可能地让开发者操作状态就像操作普通对象一样自然。

  • 订阅/通知模式。

  • 状态适用于组件之外,除了组件外,也支持用在某些逻辑上。

Zustand的特性如下:

  • 简洁性:Zustand通过一个干净直观的API简化了状态管理,减少了代码的复杂性。不需要应用外侧包裹一层Provider。

  • 性能:Zustand高度优化,为你的应用提供卓越的性能。

  • 可扩展性:随着你的项目增长,Zustand仍然易于使用并且扩展性好。

  • 不变性:Zustand鼓励不变性,使跟踪状态变化和调试问题变得更容易。

  • 灵活性:它不限于特定的框架。你可以在React、React Native或任何其他JavaScript环境中使用Zustand。

本身这两个状态库,各有千秋,没有谁好谁不好一说,只有更合适的业务的场景。考虑到我们是B端编辑器场景,Valito写法和vue相似,基于Proxy响应式的理念、以及学习成本最终使用了Valito作为我们的状态管理库

我们再聊一下画布通信,由于选用了iframe做了画布渲染的工具,所以我们通信方案也是基于iframe的postMessage。大家可以看一下流程图:

图片

核心代码渲染逻辑,大家可以参考一下。

** 高阶组件 基于iframe 进行封装 */
export const HocPreview = (Template: (componentList: any) => JSX.Element) => {return ({ data }: { data: any }) => {const [componentList, setComponentList] = useState(data?.componentList ?? [])useEffect(() => {// 在iframe中且没有hash值的时候,添加hover样式并通知父级if (window.self !== window.top && !window.location.hash) {const compElemList = document.querySelectorAll('.editor-box>div>*')compElemList.forEach((item, index) => {item.addEventListener('mouseenter', () => {window.parent.postMessage(index, '*')compElemList.forEach((citem) => {citem.classList.remove('actived')})item.classList.add('actived')})})}window.addEventListener('message', ({ data }: { data: PostComponentMsg[] }) => {setComponentList([...data])})// 去除预览页面的滚动条document.querySelector('html')?.classList.add('no-scroll-bar')}, [])const handleClick = (e: SyntheticEvent) => {if (window.self !== window.top) {e.stopPropagation()}}return (<div className="editor-box" onClickCapture={handleClick}>{/* <Mask /> */}<Template componentList={componentList} /></div>)}
}

B端组件配置

B端的form我们是基于antd的proCompoents我们选用的是SchemaForm。

SchemaForm是根据JSON Schema来生成表单的工具。SchemaForm会根据valueType来映射成不同的表单项。

除了内置valueType如图所示:

图片

https://procomponents.ant.design/components/schema#valuetype-%E5%88%97%E8%A1%A8

我们还会根据业务类型新增一些自定义的valueType。

我们的模板都有一些通用的B端column配置,因此我们将这封装成hook,所以这些通用hook,可以当做我们元数据,任何几个hook组合形成,就能搭建一个B端配置。

所以后续新增任意外投模板,可以大大节约开发的时间,因为外投模板都可以复用之前的column配置。

五、收益

  1. 提升投放业务人员的体验,提供软广服务的多样性。

  2. 积累数据效果,迭代站外落地页的效果。

  3. 潜在钱效收益:过往流程过长会发生影响如期上线的问题,大促节点如期上线抢占高下单率时机对钱效也有正向影响。

  4. 规避人工搭建跳转错误造成损失问题。

六、总结 & 规划

编辑器作为广告投放十分关键的一环,自研编辑器,离不开产品、团队同学、合作方的支持,才能推动落地。

关于编辑器后续的规划主要聚焦以下几个方面:

  1. 丰富外投落地页的通用能力,提高拉新和还原指标。

  2. 落地星创落地页数据洞察,从投放到站内承接,我们需要拿到更多的信息,能够得到更多的投放策略。

  3. 建立一套高效率的机制能快速实验和数据回收,帮助业务达成目标。

写到这里如果觉得写的不错,不要吝啬你的赞和转发。

如果你对文中有任何不理解的地方,可以在文后留言,我们可以一起技术讨论。

*文/Fly

本文属得物技术原创,更多精彩文章请看:得物技术官网

未经得物技术许可严禁转载,否则依法追究法律责任!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/341194.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

贰[2],VisionMaster/.NetCore的WPF应用程序调用控件

1,环境 VisionMaster4.2 VisualStudio2022 WPF/.Net6.0 2,记录原因 .NetFrameWork的WPF应用程序调用添加例程.NetFrameWork的Winform应用程序相应的库,不会出现报错,界面也能正常显示操作,但是.NetCore的程序却总是报错。 2.1,.NetFrameWork的WPF应用程序 注:但是.…

AI绘画SD入门教程:ControlNet篇-Canny边缘检测预处理器

大家好&#xff0c;我是向阳 在本篇中&#xff0c;我来讲讲如何使用预处理器和辅助模型&#xff0c;分别都有些什么作用。 &#x1f4a1; 这里说明一下当你调用预处理器而辅助模型显示为无的几种原因&#xff1a; 当已载入SD1.5的模型时&#xff0c;CannyXL的辅助模型不会显示…

【笔记】Sturctured Streaming笔记总结(Python版)

目录 相关资料 一、概述 1.1 基本概念 1.2 两种处理模型 &#xff08;1&#xff09;微批处理 &#xff08;2&#xff09;持续处理 1.3 Structured Streaming和Spark SQL、Spark Streaming关系 二、编写Structured Streaming程序的基本步骤 三、输入源 3.1 File源 &a…

django 内置 JSON 字段 使用场景

Django 内置的 JSON 字段&#xff08;JSONField&#xff09;是在 Django 3.1 版本中引入的&#xff0c;用于处理 JSON 格式的数据。JSONField 允许在数据库表中存储和查询 JSON 数据&#xff0c;并且在与 Python 代码交互时自动转换为合适的 Python 数据类型。以下是一些常见的…

【golang学习之旅】Go中的cron定时任务

系列文章 【golang学习之旅】报错&#xff1a;a declared but not used 【golang学习之旅】Go 的基本数据类型 【golang学习之旅】深入理解字符串string数据类型 【golang学习之旅】go mod tidy 【golang学习之旅】记录一次 panic case : reflect: reflect.Value.SetInt using…

国产打印何去何从?汉印瞄准突破口,推进发展新质生产力

推动发展新质生产力&#xff0c;已成为当前时代的主题&#xff0c;代表着先进生产力的发展方向。 打印行业因其高门槛性和技术复杂性&#xff0c;以及在信息安全领域中的作用&#xff0c;使其在我国“新质生产力”发展中占据关键位置。同时&#xff0c;打印行业融合了高精尖产…

windows10镜像文件官网下载

官网 下载 Windows 10 光盘映像&#xff08;ISO 文件&#xff09; https://www.microsoft.com/zh-cn/software-download/windows10ISO/

QT开源 串口调式工具

都是基础的代码不详细解释&#xff0c;代码比较多福利链接

【C++练级之路】【Lv.24】异常

快乐的流畅&#xff1a;个人主页 个人专栏&#xff1a;《算法神殿》《数据结构世界》《进击的C》 远方有一堆篝火&#xff0c;在为久候之人燃烧&#xff01; 文章目录 引言一、异常的概念及定义1.1 异常的概念1.2 异常的定义 二、异常的使用2.1 异常的栈展开匹配2.2 异常的重新…

Window10磁盘的分盘和合并

注意&#xff1a; 当我们c盘不够大需要扩大磁盘空间时&#xff0c;当c盘后面没有未划分的磁盘时候&#xff0c;我们是无法进行扩充c盘的&#xff0c;此时&#xff0c;我们可以先删除后面一个磁盘&#xff0c;再进行扩大。 如下&#xff1a;c盘后没有未分配的空间&#xff0c;…

6月4(信息差)

&#x1f30d;AI预测极端天气提速5000倍&#xff01;微软发布Aurora&#xff0c;借AI之眼预测全球风暴 &#x1f384;理解老司机&#xff0c;超越老司机&#xff01;LeapAD&#xff1a;具身智能加持下的双过程自驾系统&#xff08;上海AI Lab等&#xff09; 论文题目&#xf…

Flutter开发效率提升1000%,Flutter Quick教程之定义Api(三)

将tab键切换到Response&#xff0c;会出现这么一个界面 这是添加api返回的json数据。比如我们添加一个json数据。 添加完json数据后&#xff0c;右上角有一个删除按钮。要换json数据的话&#xff0c;可以点击清除再重新输入。 这时候&#xff0c;左边的面板上还会显示出 这个的…

Python实现PPT表格的编写包含新建修改插图(收藏备用)

自动创建一个ppt文件并创建好表格 代码要用到pptx库 pip install python-pptx 创建含有表格的ppt文件代码&#xff1a; from pptx import Presentation from pptx.util import Inches# 创建一个PPT对象 ppt Presentation()# 添加一个幻灯片 slide ppt.slides.add_slide(p…

用框架思维学Java:集合概览

集合这个词&#xff0c;耳熟能详&#xff0c;从小学一年级开始&#xff0c;每天早上做操时都会听到这两个字&#xff1a; 高中数学又学习到了新的集合&#xff1a; 那么Java中的集合是什么呢&#xff1f; 一&#xff0c;前言 1&#xff0c;什么是Java集合 数学集合是Java集…

Java 垃圾回收

文章目录 1 Java 垃圾回收1.1 JVM1.2 Java 对象生命周期 2 如何判断一个对象可被回收2.1 引用计数算法2.2 可达性分析算法 3 垃圾回收过程3.1 总体过程3.2 为什么要进行世代垃圾回收&#xff1f;3.3 分代垃圾回收过程 在 C 和 C 中&#xff0c;许多对象要求程序员声明他们后为其…

【第三节】C/C++数据结构之栈与队列

目录 一、数据结构-栈 1.1 栈的定义 1.2 栈的 ADT (Abstract Data Type) 1.3 栈的顺序存储结构及实现 二、数据结构-队列 2.1 队列的定义 2.2 队列的 ADT 2.3 队列的顺序存储结构与实现 2.4 优先队列 2.5 各种队列异同点 一、数据结构-栈 1.1 栈的定义 栈(Stack)可…

Web3设计风格和APP设计风格

Web3设计风格和传统APP设计风格在视觉和交互设计上有一些显著的区别。这些差异主要源于Web3技术和理念的独特性&#xff0c;以及它们在用户体验和界面设计中的具体应用。以下是Web3设计风格与传统APP设计风格的主要区别。北京木奇移动技术有限公司&#xff0c;专业的软件外包开…

CSS(盒子模型,定位,浮动,扩展)

CSS 盒子模型&#xff1a;外边距&#xff1a;内边距&#xff1a;水平居中&#xff1a; 定位&#xff1a;相对定位&#xff1a;绝对定位&#xff1a;固定定位&#xff1a; 浮动&#xff1a;扩展&#xff1a; 盒子模型&#xff1a; 盒子模型(Box Model) 规定了元素框处理元素内容…

2024最新python入门教程|python安装|pycharm安装

前言&#xff1a;在安装PyCharm之前&#xff0c;首先需要明确PyCharm是一款功能强大的Python集成开发环境&#xff08;IDE&#xff09;&#xff0c;由JetBrains公司开发。PyCharm旨在通过提供智能代码补全、语法高亮、代码检查、快速导航和重构等丰富的编码辅助工具&#xff0c…

恢复最近删除的照片!3个终极指南大揭秘!

亲爱的朋友们&#xff0c;你们有没有过这样的经历&#xff1a;一时手滑&#xff0c;不小心删除了手机里的重要照片&#xff0c;然后瞬间感觉自己的世界都要崩塌了&#xff1f;别担心&#xff0c;今天我就来给大家分享一下如何找回最近删除的照片&#xff0c;并介绍详细的方法和…