随着淘宝APP逐渐转型为生活方式APP,淘宝乐园作为互动游戏矩阵的重要入口,承担着提升用户留存与分发能力的重任。本文详细介绍了淘宝乐园新人权益活动的业务背景、需求分析与方案设计,重点探讨了消息传递与更新、引导与订阅、任务体系以及动画组件开发等通用能力的建设,旨在通过这些技术手段提升用户体验,实现用户与第三方厂商的双赢。
在上述业务背景下,淘宝乐园作为淘内互动游戏矩阵的重要入口,我们希望能够依托淘宝乐园平台对来访用户进行良好的留存与分发。通过业务运营和商业化支持,提供权益、任务、赛事运营、曝光换量等体系将淘宝乐园打造成开放游戏生态中心场,聚拢三方游戏用户规模及分发量级,提升用户回访效率。
需求分析与方案设计
该活动主要是针对特定人群投放的一个短期的(周期7天)的常规阶段性权益类活动,根据用户的任务状态来完成活动阶段的更新与权益的抽取兑换。
对于一个短期的权益活动来说,各类活动或有不同的玩法策略,但同时也存在许多共性可供归纳总结。通用活动一般都会涉及到用户人群圈定、活动机制、阶段状态变更、任务体系、订阅机制、权益发放以及活动止血等能力。而在特定的互动域下,活动中通常都需要建设新人引导、弹窗投放、消息传递与更新机制、以及通用动画组件等。
为了保证项目的可复用性与可扩展性,在前期可以初步构建一个通用的活动架构来保证后续相关需求的快速开发与迭代,因此此次需求将按照以下方案来完成代码的开发与搭建。下面将介绍其中与业务相关性弱,可抽象复用的部分功能。
▐ 通用能力建设
1. 消息传递与更新
由于互动域的特殊性,我们在淘宝乐园中经常会投放各类与互动玩法相关的运营活动来实现高效的用户留存。同时相关活动具有周期短、互动性强等特点,且互动玩法中存在高频的信息传递与更新行为。如何针对业务中投放的互动玩法快速进行迭代发布,构建一套业务内通用的消息更新与投放机制的重要性不言而喻。
消息更新与同步
a. mx状态与数据封装
mx 是一套诞生于互动搭建场景的数据流方案,提供统一的数据流转中心,同时也集成了事件通讯能力。乐园内部采用 mx 集成的事件通讯能力对事件与存储进行了统一封装管理。开发者可已通过注册事件低成本使用 mx 的事件触发与监听操作。
// 事件管理
export const Evt = {
// ...
// 游戏广场
IndexHall: getModuleEvent<IIndexHall.IEvent>('IndexHall'),
};
// 数据管理
export const Store = {
// ...
// 游戏广场
IndexHall: getModuleStore<IIndexHall.IData>('IndexHall'),
};
// 以事件触发与监听为例,数据类似
function getModuleEvent<E>(moduleName: PageName) {
return {
/** 触发事件 */
emit: function emit<T extends keyof E>(name: T, data?: E[T]): void {
mx.event.emit(`${moduleName}.${String(name)}`, data);
},
/** 监听事件 */
on: function on<T extends keyof E>(name: T, callback: (data: E[T]) => void, needMakeup?: boolean): void {
mx.event.on(`${moduleName}.${String(name)}`, callback, needMakeup);
},
/** 取消监听事件 */
off: function off<T extends keyof E>(name: T, callback?: (data?: E[T]) => void): void {
mx.event.off(`${moduleName}.${String(name)}`, callback);
},
/** 一次性监听事件 */
once: function once<T extends keyof E>(name: T, callback: (data: E[T]) => void): void {
const wrapCallback = (data) => {
callback(data);
this.off(name, wrapCallback);
};
this.on(name, wrapCallback);
},
};
}
b. 事件类型注册、触发与监听
开发者需在统一的命名空间注册后续需要触发监听的事件类型,此后按需在触发与监听事件中插入所需业务逻辑即可。
// 事件类型注册
declare namespace IIndexHall {
// ...
interface IEvent {
// ...
Event_Envelope_Init: () => void;
Event_Envelope_Open: any;
Event_Envelope_Refresh: () => void;
}
}
// 事件触发与监听 - 红包活动初始化
Evt.IndexHall.on(ENVELOPE_EVENT.Event_Envelope_Init, (data: any) => { // 事件监听
data && Evt.IndexHall.emit(ENVELOPE_EVENT.Event_Envelope_Refresh, data); // 事件触发
const activityData = getActivityData(data, TASK_TYPE.MISSION_UPGRADES_AND_EARNS_REWARDS);
Store.IndexHall.update('activityData', activityData); // 数据更新
});
消息投放
a. 弹窗
不同的运营活动有不同的弹窗投放需求,为了保证代码的可扩展性,可以规范定义弹窗统一的变量与方法,例如onConfirm()、onClose()以及data,再将不同的弹窗modal通过modalName注入到弹窗组件中,各个modal可根据各自需求定制化弹窗的内容与样式。
b. 公告
MT配置信息获取:定义Hook函数useMtConfig获取mt配置;
跑马灯CommonMarquee组件:在滚动公告的场景中,通过定义一个可扩展的跑马灯组件在应用内展示动态滚动的文本信息。定义一个通用组件接收公告文本noticeText与自定义样式style便于在不同场景下复用该组件。同时,使用useState来管理组件内定义的状态变量。
2. 引导与订阅
在各类权益活动投放的过程中,由于其互动玩法的多样性以及活动存在一定的周期性,我们通常需要在用户体验初期提供一定的引导,便于用户快速理解权益活动的互动规则。与此同时在活动投放期间,需要为用户提供订阅功能提升用户的回访效率。
showGuide({
x: px2rpx(guideBtnRef.current.offsetLeft),
y: px2rpx(guideBtnRef.current.offsetTop),
width: px2rpx(targetReact.width),
height: px2rpx(targetReact.height),
clientX: targetReact.left,
clientY: targetReact.top,
pixelWidth: targetReact.width,
pixelHeight: targetReact.height,
popText: <GuidePopComp guideText='引导文案' picList={[guideData?.benefit?.pic]} />,
popWidth: 456,
popStyle: { padding: 0, width: 'max-content' },
popHeight: 136,
clickCallback: (isClickOnTarget) => {
点击事件回调
if (isClickOnTarget) {
数据埋点
doTracker({
logkey: '/xxxx.xxxx.xxxx',
gmkey: 'CLK',
});
Evt.IndexGame.emit('Event_IndexHall_Guild');
else {
用户未点击
}
},
})
与此同时,对于权益活动来说,一个常见的场景诉求就是在平台对用户进行定点的消息投放,吸引用户进行回访。对于消费者侧,消费者有主动订阅权益活动通知提醒的诉求,希望能够主动订阅权益活动并在某些事件发生时收到提醒通知;对于业务或平台侧,也会有触达用户回访平台的诉求,因此需要业务接入订阅中心,在对应的承接页透出订阅按钮,在用户主动订阅后,发起对用户的触达通知,从而提升用户体验。
/**
* 确保异步订阅在异步查询订阅状态列表之前执行
*/
export async function subscriptionAsync() {
// 行为埋点
doTracker({
logkey: '/xxxx.xxxx.xxxx',
gmkey: 'CLK',
});
try {
await subscription();
} catch (e) {
logError(e);
}
};
/**
* 确保异步取消订阅在异步查询订阅状态列表之前执行
*/
export async function unsubscriptionAsync() {
// 行为埋点
doTracker({
logkey: '/xxxx.xxxx.xxxx',
gmkey: 'CLK',
});
try {
await unsubscription();
} catch (e) {
logError(e);
}
};
/**
* 查询并更新订阅状态
*/
export const setSubscriptionStatus = async () => {
try {
const subscriptionStatus = await checkSubscriptionStatus();
Store.IndexHall.update('subscriptionStatus', subscriptionStatus);
logInfo('订阅状态', subscriptionStatus);
} catch (e) {
logError(e);
}
}
3. 任务体系
权益活动的任务体系通用形式是以浮层为载体,在其中展示投放运营配置的五棱镜任务。因此作为业务方,我们需要提供监听浮层展示与否的能力并在合适的时机更新任务信息。与此同时,还需要与五棱镜SDK间执行信息的透传与回调,以此来搭建乐园内的通用任务体系。
对于浮层遮罩来说,我们需要尽可能地在抽象出通用功能的前提下满足多样化的功能需求。因此通过定义以下参数将浮层的展示逻辑、回调处理、内容展示等封装在组件中,减轻父组件的状态管理负担。
参数 |
是否必填 |
类型 |
备注 |
visible |
✅ |
boolean |
显示浮层 |
onShowCallback |
✅ |
() => void |
浮层显示回调 |
onCloseCallback |
✅ |
(event: any) => void |
浮层关闭回调 |
children |
❌ |
any |
浮层内容 |
panelContainerHeight |
❌ |
string |
浮层上浮高度 |
maskBackgroundColor |
❌ |
string |
浮层遮罩背景颜色 |
▐ 动画组件开发
1. 倒计时CountdownTimer
依赖导入与类型定义
首先,通过定义TimeType接口来规范时间对象的结构,包括小时、分钟、秒与毫秒等。同时引入了集成事件通讯能力的mx用于提供统一的数据流转中心,通过监听触发事件的状态变更来控制倒计时的声明周期。
状态管理
内部提供存储活动配置信息的变量activitiConfig以及记录当前活动剩余时间的变量leftTime。对外接收两个props参数,表示当前轮次的结束时间戳curRoundEndTime与用于校准客户端时间的服务器时间戳serverTime。
倒计时逻辑实现
利用useEffect监听curRoundEndTime和activityConfig的变化,响应式更新倒计时实例。同时创建两个useRef引用来持有Countdown实例,避免不必要的组件重新渲染。在onChange回调中,根据倒计时是否结束来更新剩余时间状态,如果倒计时结束,则重置时间为初始值。
这样保证了通过使用React Hooks有效管理组件状态,使得组件状态变更逻辑清晰可读。同时使用useRef维持倒计时实例,避免因组件状态改变导致的不必要的实例重建,提升了组件性能。监听props变化自动启动或更新倒计时,实现了动态响应外部数据的更新。除此之外正确处理了事件监听的注册与清理,用于防止内存泄漏。
2. 飞入动画FlyAnimation
动画起始位置与目标位置计算
在计算元素位置之前我们需要统一可接收的坐标规范,并在考虑到页面滚动、元素偏移等影响因素的情况下基于给定规范的坐标位置计算元素的moveX与moveY值。
动画执行逻辑
制定合理的动画执行单次时长与总时长、飞行元素个数与缩放效果,同时按需选择缓动类型并逐步更新元素的位置信息。利用useCallback与useMemo减少不必要的计算与重渲染动作,并通过useEffect管理动画播放完毕后定时器的清除工作。
规范的动画生命周期
通过在外部函数中动态创建DOM元素,挂载React根实例,并使用Promise来管理动画的生命周期,使得外部可以感知动画是否播放完毕,方便与其他业务逻辑集成。
/**
*
@param params
@ 开始位置 startPos: { x: number; y: number }| HTMLElement;
@ 结束位置 endPos: { x: number; y: number } | HTMLElement;
@ 动画总时间 during?: number; 单位:毫秒
@ 飞行的图片 flyImage?: string;
@ 飞行图片大小 flyImageSize?: { width: number; height: number };
@ 飞行个数 flyNumber?: number;
@ 飞行间隔时间 singleDelay?: number;
@ 最终缩放值 endScale?: number;;
@ 起点是否考虑屏幕滚动 withScreenStart?: boolean;
@ 终点是否考虑屏幕滚动 withScreenEnd?: boolean;
@ 图片做不做圆角处理 isFlyComShapeRound?: boolean;
*/
await FlyAnimationComponent({
startPos: document.getElementById(`envelope${level}`) || { x: 0, y: 0 },
endPos: document.getElementById('envelopeAmount') || { x: 343, y: 690 },
during: 800,
flyImage: ICON.open_envelope,
flyNumber: 5,
singleDelay: 0.1,
endScale: 0.6,
});
相较于解决技术难题或设计通用SDK来说,做业务需求有很多跟业务强相关的逻辑编写更为复杂。开发前的业务逻辑梳理与需求的技术方案设计尤为重要。在互动域下,业务活动通常都存在业务逻辑复杂,运营周期短的特性。随着需求迭代,业务代码的不断堆积会使得工程变得臃肿、冗余,增加了维护难度。如何在每次业务需求中提炼出通用能力,并设计可扩展且易用的功能组件是日常业务开发中需要着重思考的地方。
本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。