背景
大家好, 我是Fly哥, 这次分享的内容主要是关于ABtest ,我们是做用户增长的,说白了就是对应下面几个关键词。拉新、激活、留存,留存的话 又分为 次日留存、 3日留存,这些都是我们的指标, 但是产品设计一个需求的时候, 可能会有实验的性质,不确定哪一组实验,对于指标的反馈是正向的,或者是那一组实验的效果更加明显。 这时候产品就会去创建AB实验,然后拿线上的一部分流量,去做实验, 分析数据, 得出实验结论,然后看是否满足预期, 如果不满足 就暂停实验, 或者进行全量实验。 大家可以看下下面这张流程图:
然后对于我们前端而言,我们关心的点只有两个
- 第一个就是接入ABtest,然后去做逻辑处理, 根据当前用户命中的不同实验,走不同的逻辑
- 第二个就是实验结束后的操作,不管是应用实验 还是 暂停实验, 那么对于我们而言都是进行代码删除
ab实验是什么??
可能很多同学讲到这里还是有点模糊,我在这里举一个最简单的例子, 我们在实际开发过程中的
我们通常在页面有样式变化,但又不确定是修改影响好坏的时候进行 AB 实验,实验数据可以为改版提供有力支持。
某公司日常(1)产品:把页面标题变成之前的两倍。标题就是要醒目,要大大大
image-20220604101406387
设计:打死都不同意,太大的标题不精致不优雅。 BOSS:下面的按钮点击导流就是收入。收入就是一切!!!你们做下实验,采用收入高的方案。 前端:哦。
从上面的图片 可以看出, 其实我们采取了AB 实验, 对于不同的用户,页面可能展示 不一样, 其实就是选取线上一部分流量,去进行实验, 然后划定目标人群, 对比数据的变化, 最终采取是否运用。
如何接入ABtest
我们先看下第一个问题, 前端如何ABTest。 我们看下老的接入AB流程图, 基本上是 产品创建一个AB 实验, 后端同学就要写一个接口, 我们去调用。 然后得到的该用户命中的分组信息,然后我们在根据逻辑判定。 如下图:
abtest1.
通过与产品交流得知, 目前得物的AB 实验平台分为下面三种
- 客户端实验
- 后端实验
- H5实验
用户的分组信息, 以及实验的名称都是产品在 AB 实验平台进行创建的,产品会在prd上
写上, 如:
key: 'xxxx', value: 'old' 对照组 value: 'new1' 实验组1 value: 'new2' 实验组2
而后端获取用户AB信息 也是AB平台提供的分流接口, 然后在转发给我们, 所以后端接口起到了一层中间层,那就没必要了,这个链路是可以优化的,我们可以在创建实验的时候,拉上产品和后端同学进行沟通, 确认你是不是需要用户AB的信息,进行逻辑处理,如果完全不需要的。那么产品创建的时候,就创建H5 实验, 然后我们就直接调用AB 平台提供的H5接口,获取AB 信息就可以了。 如图所示 :
一个实验倒还好, 但是如果有很多实验, 并且对应的是不同的业务, 所以没办法保证每个开发接口的同学,保证接口的返回值统一,有的返回一个布尔值, 有的返回字符串。 但是这部分逻辑就是通用的,所以当后端不需要拿到用户的AB 信息,去对不同的用户 做逻辑处理的话, 我们就可以不走他们接口,将这部分逻辑统一封装到sdk 中,方便各个业务线调用,专注业务开发,不需要关心用户的分组信息
当时这么设计的原因主要有下面几点: 因为团队的技术栈有laya, cocos, react,他们在渲染层的处理方式都不一样, 所以分了两个npm包一个负责逻辑处理,一个负责渲染层, 做到解耦。 因为AB 平台后续会支持H5实验,传入 多个key ,得到当前用户在多组实验对应的分组信息,所以对应的设计图如下:
代码删除
谈到做AB 实验, 我拿我目前负责的一款游戏点鞋成金举例子: 产品要对游戏的头部进行做实验,看是不是直接凸出奖励,对于用户的吸引力更大
分别对应3个组 第一个是对照组:
第二个是实验组1:
第三个是实验组2:
如果要要去开发的话其实就会这么去写:
if (info === 'A') { // 实验A 用户的展示逻辑 } else if (info === 'B') { // 实验B 用户的展示逻辑 }else if(info === 'C') { // 实验C 用户的展示逻辑 }
其实看上去还好对吧, 不就是多写几个 if else , 但是你 随着产品的实验 越来越多, 你这样的代码会越来越多。 会带来下面这 几个问题
- 当实验全部应用一个 数据比较好的 情况下, 你的那些脏代码,是不是要全部删除。这个需要我们前端 还要 花时间 去定时清理
- 因为虽然你可能只是删除了代码,在上线的时候其实也需要测试 进行回归, 万一你误删了,可能你和测试都不知道,造成线上事故。
- 这两者 其实都 占用了 开发 和测试的一些时间 ,怎么做到提高代码的可维护性和稳定性呢
cocos
这里我介绍下cocos 方面我是怎么处理的, 用cocos 做的游戏, 页面每一个元素都叫做一个node ,每个节点上都有一些基础的属性,比如设置节点的 大小, 缩放, 锚点,还包括绑定脚本,绑定各种各样的组件 等等。如图所示:
其实我们可以开发一个自定义AB组件, 然后在脚本设置可以在编辑器中使用, 这样每个节点就可以添加到我们自定义的脚本组件。
// 编辑器特性 editor: { // 允许当前组件在编辑器模式下运行 executeInEditMode: false, // 当本组件添加到节点上后,禁止同类型(含子类)的组件再添加到同一个节点,防止逻辑发生冲突 disallowMultiple: false, // menu 用来将当前组件添加到组件菜单中,方便用户查找 menu: 'NX/FLEX/AB', },
然后内部我们通过全局的视图绑定, 绑定到AB请求的返回值,去做页面的渲染, 对于我们而言就不需要判断, 页面渲染哪个组件了。内部逻辑都封装好了,我们只需要关系当前实验组的表现是怎么样的就好了。 如图:
image-20220528164215375
我介绍下这几个节点的意思
- container 节点只负责绑定自定义脚本AB组件,
- 然后container 下面的子节点 对应的其实就是不同的实验分组, 展示的不同node, 比如 命中了实验组1 那么我页面展示其实就是 展示 headerA 其他节点都隐藏
我们看下container 节点的绑定
点击下方的添加组件,然后找到我们对应的脚本组件AB如图:
然后我们根据提示信息,填写对应的配置就完成了AB 实验的搭建了, 页面 就可以渲染我们想要的结果了
这里的key 和value 都是由产品提供的,我们填写就好了, 然后实验应用了, 我们只需要把对应的节点删除了就好了, 代码层面不需要做任何改动。
react
我们在看看react 层面 我是如何去做的,这里是由组合组件的方式去实现的 一个
<ABTestComponent config={{ keyName: 'xx' }} fallbackComponent={renderDefault()}> <Variant name={'new1'}> <New1 /> </Variant> <Variant name={'old'} /> </ABTestComponent>
一个组件 ABTestComponent 和一个 变量 组件 Variant,这里为什么设置两个组件, 当时是这么考虑的 由于产品设置的实验组 可能有多个, 无法确定一个实验key 对应多少个value 。 所以干脆利用 react 的 props.children 去match 对应的子组件, 然后渲染就好了。
ABTestComponent 这个实验 你只需要传入 当前的实验key 就可以了,然后传一个兜底组件,这是边界情况考虑, 内部 做了兜底处理,防止页面崩溃。
然后 Variant 这个组件下面封装各种情况的组件, 你就不需要 大量的if else 了, 这个场景 比较单一可能还看出优势,我们在看下一个稍微复杂的场景如下:
<ABTestComponent config={{ keyName: 'xx' }} fallbackComponent={renderDefault()}> <Variant name={'new1'}> <ABTestComponent config={{ keyName: 'yy' }} > <Variant name={'new3'}> <New3 /> </Variant> <Variant name={'new4'}> <New4 /> </Variant> </ABTestComponent> </Variant> <Variant name={'old'} /> </ABTestComponent>
我这个用户 命中 实验xx 的new1 并且 在new 1的情况下 又分为 new3 和 new4 做一进步实验, 然后要做各种逻辑处理。用这种方式去处理的话,逻辑都已经分开了, 你只需写对应的展示 就好了, 如果应用了那一组, 你只需要在 组件 这块做删除就好了, 不用在各种方法删除, 提升代码的可稳定性 或者维护性。