本人郑重声明:
作为多篇前端专利拥有者,公司专利提交主力军,本人公开发表的专利样本、原本文章均为未成功发布的专利,其中可能原因为创新点不足、功能逻辑未闭环或者缺少资料补充等等,此类文章未构成法律侵权。
也慎重提醒大家,正式专利受国家专利法保护,引用需按要求来,绝对禁止直接搬运交底书、专利原文。
摘要
互联网 C 端产品在不确定哪种方案更有利于达成核心目标时通常通过做 A/B 实验来进行测试,从而达成数据驱动创新,数据驱动增长的目标。然而,伴随着 A/B 实验的进行,带来的负担是请求变多,资源消耗过大会给服务器造成很大压力,严重情况下出现宕机。
针对以上情况,本文提出了一种动态接口请求管理方案,该方案通过新建 AB 管理器和资源管理器两个管理器实现。
首先,由 AB 管理器读取用户缓存 A/B 值或者默认 A/B 值并通知资源管理器;之后,资源管理器根据 A/B 值动态生成 API 列表并直接进行资源拉取,此时请求数量和参数数量会得到一定数量的减少,尤其在 A/B 实验为非散列且 AB 管理器读取缓存初始化的情况下,可以达到最佳优化效果。最后,AB 管理器会在初始化后同时拉取远程最新 A/B 值,在真实 A/B 值发生变化时通过 diff 和增量机制,资源管理器再次发起补充请求,保障数据时效性,同时避免重复和无意义的流量损失。
通过在实际项目中应用验证,该方案稳定可靠,可以运用到实际应用中。
关键词:A/B 实验; 请求管理;动态接口;AB 管理器;资源管理器;增量通知
核心优势:
1. 动态接口请求,Web页面并非和其他方案一样每个用户、每次打开页面都发起全部请求,而是根据不同的用户在不同时间命中的 AB 实验值,动态的发起仅需要的请求大幅度减少重复和无意义的流量损失,增加 web 页面的性能和减少服务器的压力。
2. 采用缓存技术不可避免的存在缓存过期的情况,本文在真实 A/B 值发生变化时通过 diff 和增量机制,再次发起补充请求,做到以最小程度的代价进行更新以保障数据时效性,可以有效的避免重复和无意义的流量损失
3. AB资源管理器故障时,资源管理器依然可以独立工作,做到渐进增强,有安全保障
一、问题提出
互联网 C 端产品在不确定哪种方案更有利于达成核心目标时,通常通过做 A/B 实验来进行测试,从而达成数据驱动创新,数据驱动增长的目标。
然而,在 A/B 实验场景下,发起资源请求通常需要依赖 A/B 的结果值,但因为网络情况不同、业务场景多样化和嵌套 A/B 等原因,通常不会串行发起请求,即等待 A/B 请求结果后再进行下一步,这无疑会对应用的性能造成极大的影响,用户体验上也会出现打开页面缓慢、资源加载不及时或看不到内容等情况。在这种情况下,通常的做法是 A/B 请求和其他资源请求并行发起,因此,每个用户、每次打开页面都会向服务器发起全部资源请求,在拿到全部资源结果之后再由 A/B 值决定使用哪种资源,但这样的方案无疑会造成请求变多,资源消耗过大。
伴随着大量 A/B 实验的进行,请求资源变的越来越多,资源消耗过大会给服务器造成很大压力,严重情况下出现宕机。
在解决方案中,不仅要面对实时性的要求,还要考虑流量的稳定性,这就给接口请求管理带来了很大的难度。
二、场景假设
以下分析均假设 Hybrid App 为 APP + H5离线包的场景
假设课题场景假设在 Hybrid APP 中的页面 P 进行,存在以下几个 A/B 实验:
1. P 页存在两种样式,由 A/B 实验 code“pageStyleAB”决定使用哪种样
l 命中 A 组的使用样式1,接口返回为 { pageStyleAB:A },样式1包含有 4 个资源接口请求,分别为 API1,API2,API3,API4
l 命中 B 组的使用样式2,接口返回为 { pageStyleAB:B },样式2包含有3 个资源接口请求,分别为 API4,API5,API6(其中 API4 为通用接口)
(例如,样式1为普通用户样式,样式2为vip用户样式,不同用户进入Web 页面所展示的页面不同,二者请求页面资源的接口也不同,其中,API4(相同接口)是全部类型用户均能看见的内容)
2. API1 接口为广告列表接口,总计有 10 个参数分别代表10个广告,参数名为:AD1,AD2,AD3,AD4,AD5,AD6,AD7,AD8,AD9,AD10。调用情况由 A/B 实验code“adAB”决定,其值可能为 A、B、C
l 命中 A 组的用户,接口返回为 { adAB:A }, 参数为AD1,AD2,AD3,AD4
l 命中 B 组的用户,接口返回为 { adAB:B }, 参数为AD5,AD6,AD7,AD8
l 命中 C 组的用户,接口返回为 { adAB:C }, 参数为AD9,AD10
3. API4 接口为最热搜索列表接口,总计有 8个参数,分别代表8个最热搜索词,参数名为:Q1,Q2,Q3,Q4,Q5,Q6,Q7,Q8。调用情况由 A/B 实验code“searchAB”决定,其值可能为 A、B
l 命中 A 组的用户,接口返回为 { searchAB:A }, 参数为Q1,Q2
l 命中 B 组的用户,接口返回为 { searchAB:B }, 参数为Q3,Q4,Q5,Q6,Q7,Q8
此时在未知用户命中 A/B 实验值的情况下,用户每次进入 Web 页面都必须发起全部请求拉取资源,即
接口 | 参数 |
---|---|
API1 | AD1,AD2,AD3,AD4,AD5,AD6,AD7,AD8,AD9,AD10 |
API2 | 略… |
API3 | 略… |
API4 | Q1,Q2,Q3,Q4,Q5,Q6,Q7,Q8 |
API5 | 略… |
API6 | 略… |
总计 10 个接口,18 个参数。全部资源请求回来之后,再由AB 实验值决定使用哪些资源。
显然这造成了资源浪费,假设有一个命中 pageStyleAB = A 且 adAB = A的用户实际上只需要 API1,API2,API3,API4 共4个接口,AD1,AD2,AD3,AD4,Q1,Q2, 共6个参数。
(ps:AB 实验的值从服务端获取也需要从服务端接口远程拉取,此接口在这章节这里忽略了)
三、解决方案(发明内容)
Web 端 A/B 实验通常是非散列的,即其值和设备 ID 或者用户 ID 绑定,因此其一旦确定值之后大部分时间内都不会再次变化,直到业务方再次调整 A/B 策略或者关闭 A/B 实验为止。
结合以上特性,在首次用户访问全量请求后,通过缓存 A/B 结果和动态生成请求列表两个手段,最后通过 diff 算法保证 A/B 实验变化后的场景,从用户第二次访问开始,可以大幅度减少请求数量和参数,从而减少服务器压力。
3.1 主要步骤
基于以上分析,方案架构上分为两个主要模块 AB 管理器和资源管理器,AB 管理器负责所有 A/B 相关的事情并在 A/B 值更新后发出通知,资源管理器负责由 A/B 结果动态生成接口列表,并发起请求拉取资源。
(PS:本申请的AB管理器和资源管理器均位于客户端)
3.1.2 AB 管理器工作详细步骤
1. 代码中写入要管理的 A/B 实验code列表,即为:pageStyleAB,adAB,searchAB(此页面所有在进行的 AB 实验,提前在开发阶段即可确定,写死在代码里即可)
2. 用户每次进入 Web页面后,根据 A/B 实验code列表优先从本地缓存获取 A/B 数据,并把AB实验数据作为值通知资源管理器进行资源更新,并把此AB实验数据作为 AB 管理器的已管理数据进行存储(内存存储),以方便后续进行 diff 对比
a) 如果缓存有数据,则AB管理器直接进行初始化,并发出 update 事件进行通知,通知的值即为 缓存的 AB 实验code 值
b) 如果缓存不存在,则使用默认值进行初始化,并发出 update 事件进行通知(通常为首次启动 APP)
3. 与步骤 2 同时,AB 管理器开始发起AB实验接口请求,从服务器拉取远程真实 A/B 数据
a) 拉取动作如果超时或者发生错误,说明用户当前处于网络不佳、无网等情况,则 AB 管理器工作终止
b) 如果拉取动作成功,则和已管理AB实验数据进行 diff 对比,如果对比发现AB实验列表值有变化,则把发生变化的 A/B 结果通过 update 事件进行增量通知; 如果无变化,则AB 管理器工作终止
(ps:即依次对比远程返回的AB 列表中pageStyleAB,adAB,searchAB的值,和已经管理的值的区别,如果相同则忽略,如果变化比如 adAB以前是 A,现在变成了 B,则记录 adAB = B,并作为增量数据进行 update 通知)
4. 全过程中发生任何错误,管理器发出 error 事件,告知资源管理器
AB 管理器在整个流程中为一个辅助角色,主要作用是负责管理 Web 页面所有相关的 AB 实验结果输入和输出,使其统一化,方便后续核心资源管理器进行工作。
3.1.3资源管理器工作详细步骤
1. 收到 AB 管理器的 update 事件的情况下,根据收到的 A/B 列表值定制化生成接口列表(API List)
2. 示例中,假设A/B 实验 code列表值为 pageStyleAB = A,adAB = A 且searchAB = A则 API List 如下,总计 4个接口,6个参数
接口 | 参数 |
---|---|
A1 | AD1,AD2,AD3,AD4 |
A2 | 略… |
A3 | 略… |
A4 | Q1,Q2 |
假设A/B 实验 code列表值为 pageStyleAB = B,adAB = A 且searchAB = A则 API List 如下,总计 3个接口,2个参数
接口 | 参数 |
---|---|
A4 | Q1,Q2 |
A5 | 略… |
A6 | 略… |
假设A/B 实验 code列表值为 pageStyleAB = A,adAB = B 且searchAB = A则 API List 如下,总计 4个接口,6个参数
接口 | 参数 |
---|---|
A1 | AD5,AD6,AD7,AD8 |
A2 | 略… |
A3 | 略… |
A4 | Q1,Q2 |
3. 获取到 API List 之后,根据列表中的所有接口和参数值,向远程服务器发起请求获取相关资源数据(样式、广告、热搜词)
4. 获取服务器返回的资源数据成功后,资源管理器发出 update 事件通知Web页面进行渲染
5. 继续接收 AB 管理器的 update 事件,第二次接收开始 AB 实验列表数据为增量数据,重复以上 2,3,4步骤进行增量生成 API List 和拉取数据动作(只在 A/B 结果发生变化时执行)
6. 收到 AB 管理器的 error 事件的情况下,发起全量接口列表(API List)请求(即总计原始全量的 10 个接口,18 个参数),保证在 AB 管理器故障的情况下 APP 依然可以正常工作
由上方案得出,用户在首次打开 APP 时使用 A/B 默认值初始化 AB 管理器,之后直接进行资源拉取,此时对比全量请求的场景,请求数量和参数数量已经可以得到一定数量的减少,之后再次进入 APP 时,使用缓存数据进行初始化,最大程度做到用户适配或者设备适配,第二次请求时请求数量和参数数量均可达到最佳效果。可以大幅度减少重复和无意义的流量损失,增加 web 页面的性能和减少服务器的压力。
综上,该方案全部过程都由 diff 和增量机制保障在真实 A/B 值发生变化时再次发起补充请求,避免重复和无意义的流量损失,并且在 AB 资源管理器故障时,资源管理器依然可以正常工作。
方案时序图如下:
3.2 核心优势
4. 动态接口请求,Web页面并非和其他方案一样每个用户、每次打开页面都发起全部请求,而是根据不同的用户在不同时间命中的 AB 实验值,动态的发起仅需要的请求大幅度减少重复和无意义的流量损失,增加 web 页面的性能和减少服务器的压力。
5. 采用缓存技术不可避免的存在缓存过期的情况,本文在真实 A/B 值发生变化时通过 diff 和增量机制,再次发起补充请求,做到以最小程度的代价进行更新以保障数据时效性,可以有效的避免重复和无意义的流量损失
6. AB资源管理器故障时,资源管理器依然可以独立工作,做到渐进增强,有安全保障
3.3 管理器在数据流向中的位置
3.4 AB 管理器架构和实现
3.4.1 AB 管理器架构图
3.4.2 AB 管理器实现代码
import { isObject } from '@/utils/is';
import jsbridge from '@/utils/jsbridge'
import { getABUrl, getDataByTimeout, getDiffABResult } from '@/utils';
export class ABManager {
cache = localStorage; // TODO: 暂时先用 LocalStorage,后期改用自定义加密缓冲区
#cacheKey = ''; // 本地缓存 key
#codes = []; // 需要的 ab codes
#result = {}; // 当前结果值
#defaultResult = {}; // 结果默认值
#listeners = {}; // 监听事件回调函数队列
#eventTypeWhiteList = [ // 可监听事件白名单
'error',
'update'
];
constructor(config) {
this.#codes = config.codes;
this.#cacheKey = config.cacheKey;
if (config.defaultResult) this.#defaultResult = config.defaultResult;
// 异步获取远程数据:优先通过 JSBridge 获取,超时后通过 API 获取
getDataByTimeout(
this.updateResultByJSBridge(), // JSBridge 获取 AB
this.updateResultByFetch(), // API 获取 AB
300, // 超时时间
).then(res => {
this.#result = getDiffABResult(this.#result, res); // 获取差异增量结果
this.update(); // 发出更新事件
});
// 同时从本地缓存获取数据,如果不存在缓存则使用默认值
let cacheResult = {};
try {
const cacheData = this.cache.getItem(this.#cacheKey);
cacheResult = cacheData ? JSON.parse(cacheData) : {};
if (!isObject(cacheResult)) cacheResult = {};
} catch (error) {
cacheResult = {};
}
const result = {};
for (const code of this.#codes) {
const defaultResult = this.#defaultResult[code] ? this.#defaultResult[code] : 'A';
result[code] = {
value: cacheResult?.[code]?.value ? cacheResult[code].value : defaultResult
};
}
this.#result = result;
this.update(); // 发出更新事件
}
/**
* @description: JSBridge 获取AB结果
* @param {*} codes
* @return {*}
*/
updateResultByJSBridge() {
return new Promise((r) => {
jsbridge.call('base.getCacheByCode', this.#codes, (res) => {
r(res);
});
})
}
/**
* @description: 接口获取AB结果
* @param {*} codes
* @return {*}
*/
async updateResultByFetch() {
return fetch(getABUrl(this.#codes));
}
/**
* @description: 更新数据
* @return {*}
*/
update() {
this.#emit('update', this.#result);
}
#emit(eventType, data) {
if (!this.#eventTypeWhiteList.includes(eventType)) {
throw new Error(`未支持 ${eventType} 监听`);
}
const callbacks = this.#listeners[eventType];
if (callbacks) {
callbacks.forEach(c => {
c(data);
});
}
}
on(eventType, listener) {
if (!this.#eventTypeWhiteList.includes(eventType)) {
throw new Error(`未支持 ${eventType} 监听`);
}
if (!this.#listeners[eventType]) {
this.#listeners[eventType] = [];
}
this.#listeners[eventType].push(listener);
}
clear() {
this.#listeners = {};
}
}
3.5 资源管理器架构和实现
3.5.1 资源管理器架构图
3.5.2 资源管理器实现代码
import jsbridge from '@/utils/jsbridge';
import { getApiListByAB, getDataByTimeout } from '@/utils';
export class ABManager {
#abCodes = []; // ab 值列表
#apiList = []; // 发起请求的 api 列表
#listeners = {}; // 监听事件回调函数队列
#eventTypeWhiteList = [ // 可监听事件白名单
'update'
];
constructor(config) {
this.#abCodes = config.codes;
this.#apiList = getApiListByAB(config.codes);
for (const apiConfig of this.#apiList) {
getDataByTimeout(
this.updateResultByJSBridge(apiConfig), // JSBridge 获取资源
this.updateResultByFetch(apiConfig), // API 获取资源
300, // 超时时间
).then(res => this.update(res))
}
}
/**
* @description: JSBridge 获取资源
* @param {*} codes
* @return {*}
*/
updateResultByJSBridge(apiConfig) {
return new Promise((resolve) => {
jsbridge.call('base.getCacheByCode', apiConfig, (res) => {
resolve(res)
});
})
}
/**
* @description: API 获取资源
* @param {*} codes
* @return {*}
*/
async updateResultByFetch(apiConfig) {
return fetch(apiConfig);
}
/**
* @description: 更新数据
* @return {*}
*/
update(res) {
this.#emit('update', res);
}
#emit(eventType, data) {
if (!this.#eventTypeWhiteList.includes(eventType)) {
throw new Error(`未支持 ${eventType} 监听`);
}
const callbacks = this.#listeners[eventType];
if (callbacks) {
callbacks.forEach(c => {
c(data);
});
}
}
on(eventType, listener) {
if (!this.#eventTypeWhiteList.includes(eventType)) {
throw new Error(`未支持 ${eventType} 监听`);
}
if (!this.#listeners[eventType]) {
this.#listeners[eventType] = [];
}
this.#listeners[eventType].push(listener);
}
clear() {
this.#listeners = {};
}
}
四、可替代方案
1. Hybrid APP 环境可以替换为任意浏览器环境
2. C 端产品场景可以替换为任意使用 A/B 实验的前端场景
3. 本地缓存可以是任意前端缓存,如 Localstorage、indexDB 等
4. Web 端可以换成任何非散列型 A/B 实验场景
5. 散列 A/B 实验场景也可应用,效果略差
6. 超时机制可以替换为竞速机制、等待机制等
五、结论
针对 A/B 实验场景下流量消耗过大的问题,提出了一种动态接口请求管理方案。首先,通过 AB 管理器独立管理 A/B 结果,在合适的时间告知资源管理器当前常规的 A/B 列表值或者增量的 A/B 列表值;其次,资源管理器收到 A/B 列表值后动态生成请求接口列表或者增量接口列表,并发起请求拉取数据;最后,APP 根据数据进行页面渲染。
这种方案下,在用户首次启动 APP 即可大幅度减少请求数量和参数,之后再次启动 APP 可以进一步减少,做到最佳适配。并且在 AB 管理器发生错误时,资源管理器依然可以独立工作,做到安全保障。
该方案已经在实际项目中应用,结果表明能够满足实时性和稳定性要求,是一个可靠的方案。
关键术语解释和技术缩略语:
(新增)关键术语解释和技术缩略语(下文出现的英文缩写术语在此提供对应的英文全称和中文译文):
1. A/B 实验
简单来说,AB实验就是“对照”,过程分三步:首先设置实验组和对照组,然后用一段时间来收集数据,最后比较二者的不同效果。所以,AB实验也叫“对照实验”。通常来说,其表现形式为一个 AB code 码对应一个 AB 值,例如:web 页面中的“百亿补贴”活动卡片,命中 AB 实验中 A 组的用户显示,命中 B 组的用户不显示。其 code 码为 billionSubsidy,值为A 或者 B。那么用户1通过服务端接口拿到的可能是 { billionSubsidy:A },用户2通过服务端接口拿到的可能是 { billionSubsidy:B },这样,A 用户进入 Web页面时则展示卡片,B 不展示。
2. AB 管理器
AB 管理器,即 AB 实验管理器,其作用为辅助,负责管理 Web 页面所有相关的 AB 实验结果输入和输出。首先,通过一定的策略拿到Web页面所有相关 AB 实验的值,即为输入;之后,把这些值的获取接口通知给资源管理器。
3. 资源
即Web页面渲染所需要的数据、图片、文本等动态资源,通常由服务端接口获取。
4. 资源管理器
资源管理器,负责管理 Web 页面所有相关资源的输入和输出。首先,通过 AB 实验值生成需要发起请求的接口列表,即为输入;之后,按照列表,通过一定手段获取到相关资源,以供页面渲染。
5. 动态接口请求管理
本文所述的动态接口请求,其动态性主要是指Web页面并非和其他方案一样每个用户、每次打开页面都发起全部请求,而是根据不同的用户在不同时间命中的 AB 实验值,动态的发起仅需要的请求。
6. 渲染
即浏览器把数据按照一定的样式进行解析、组合,最终绘制成用户看到的页面的过程
7. 接口
是指 API 接口,本文特指在客户端通过 HTTP 协议和服务器端进行交互时由服务端提供的调用方法。服务端通过 API 接口来实现特定的功能,而调用者不需要了解其内部实现细节。也可以把 API 接口理解为是对特定服务(service)的一种封装,将服务封装起来提供给他人调用,这样不需要重新开发即可实现功能,也可以很好的保护数据的安全。例如前端请求调用后端接口来实现对数据的增删改查。
8. Hybrid App
为实现快速迭代、跨平台、动态更新、减体积、无限扩展等能力,现在市面上很多 APP 采用 Hybrid 开发方案,即原生 + H5,通过 WebView 加载 H5 页面来实现需要高度灵活性的业务,在需要和 Native 做交互或者是调用特定平台能力时再通过 JsBridge 来实现两端交互。
9. 接口参数
接口参数指的是接口方法的参数列表。在接口中定义的方法通通常需要一些参数来完成特定的操作。例如,一个接口用于实现文件读取操作,其方法可能需要一个文件名作为参数。
Q&A
Q1 :这里的资源是指什么?
A : 这里的资源指网页上的任何一个组成元素,可大可小,可以是一个按钮、一段文字或一张图片等等,也可以是具有独立功能的元素组合体,比如限时秒杀、9.9特卖、弹窗、banner、二楼、通知栏、头条轮播图、搜索热词、排行榜、气泡等等,通常会根绝不同业务进行定义。
资源通常特指动态化的元素,即展示内容通过发送 HTTP 请求从后端服务器远程获取。举个例子,一张图片,图片地址是通过后端服务器返回。
Q2 :是一个网页对应着一个资源吗?还是一个网页中的不同位置对应着多个资源?又或者是一个网页中的不同元素对应着多个资源?
A : 这三个问题统一回答下。一个网页中存在多个资源,根绝 Q1 对“资源”的名词解释可知,具体怎么区分一个资源,需要看业务自己的定义或者说感兴趣的点进行划分。也可以粗略的认为网页上的任意一个元素都是资源。
Q3 :接口是指什么,远程资源数据对应的网页接口吗?
A : 接口这里指代 API 接口,API接口大多数是由后端编写,前端开发人员进行请求调用。即客户端向服务器端发起 HTTP 请求,有后端返回远程资源或者进行某种动作,本文指代获取服务器远程资源数据
针对检索报告文件1审查员理解的解释:
对比文件1《一种前端数据模拟的实现方法及系统》描述了一种前端mock 数据的方案,其核心思路是通过共享服务器提供接口列表,首先内置默认接口数据,之后定期轮训更新这些数据。
其与本方案存在根本区别:
1. 其实现方式,本质上是一种轮训更新方案,其仅更新服务器数据,无法做到单个用户定制化,实际情况中在 AB 实验场景下每个用户的每次打开Web页面所需要发起的资源请求都是不同的。因此数据模拟系统是一个测试环境的产物,无法用于生产环境。
2. 其是使用轮训机制实现更新,即便再小的轮训间隔也无法保障实时性,本文方案中检查时机在真实服务器接口返回时,且只在最佳时间仅检查一次,拥有最佳时效性,也无须多次检查。
3. 其只能增加/减少接口列表,无法做到完全动态化适配,完全动态化的接口列表配置应该是根据不同情况,不同场景,不同用户,给出多种不同的列表方案,比如用户1接口列表为 A、B,用户2接口列表为 C、D
4. 其无法做到请求参数的动态化管理,只是接口层面的一个方案,因此无法真正减少服务器端甚至数据库的增删改查
5. 在其方案下,需要 diff 的是全部资源请求,效率低下,本文方案仅需要 diff AB结果,效率极高,由 AB 结果决定资源请求
6. 其 diff 过程发生在资源请求完成之后,此时已经发起了全量请求,对减少服务器压力没有任何帮助。本文方案diff 过程发生在资源请求完成之前,真实发起的请求已经是经过动态定制化的,可以大幅度减少远程服务器的压力
7. 其存储数据在服务端而非客户端,这就首先造成了服务器资源浪费,其次无法做到每个用户、每台设备缓存层面的定制化。本文方案存储和方案都在客户端
8. 严格来说其方案并不是动态生成请求列表,而是无限更新资源。本文方案主要特点是动态化为每个用户、每台设备甚至每次方案提供定制化的请求列表。因此文件1方案和本文方案目标也不一致
9. 从实现角度上讲,都是缓存 – 更新,但文件 1 只是简单地缓存 + 更新。本文则是实现了一套完整的系统,缓存为在用户客户端的定制化缓存,并且只缓存 AB 结果,在定制化缓存的基础上再进一步动态的生成接口列表
10. 本文方案仅是缓存 A/B 结果,和文件1的缓存全部文件思路完全不同