电商大促的活动页面根据用途可分为,会场类和互动类。虽然同为活动页面,但是两者在开发模式、交互方式上存在巨大的差异。去年,我们团队从会场类开发团队转为互动类开发团队,踩过了不少坑,也积累了一些宝贵的经验。本文将以 2020年大促互动——城城分现金(下文简称分现金)为例,分析会场类和互动类页面开发差异;针对互动开发中遇到的难点,从提炼抽象模块、小程序工程化等方面给出自身的解决方案,希望可以帮助到有同样困扰的读者朋友们。
整体来看,会场页面比较偏向于展示,交互主要集中在领券、跳转等简单操作上,因此很容易实现复用。我们团队的通天塔系统通过可视化搭建的方式,能够支持大促期间绝大多数的会场页面,可以将宝贵的研发资源从重复开发中释放出来,支持更多的项目。
而互动页面,更像是一个小游戏,偏向于交互,交互形式也是多种多样,包括但不限于动画、拖动、弹框、toast 等。不同的活动,可能在展示形式、玩法设计、交互方式上完全不一样,如何去提炼共通的部分,实现代码复用是个难点,这一部分会在接下来的内容中详细讲到。
相较于会场类平铺展示(以 y 轴方向延伸),互动类更偏向于 Z 轴方向层叠,层次关系更为复杂。我们可以借鉴游戏的分层思路,对互动页面进行分层。
页面层级图
背景层:背景图,页面底色
动画层:也是主体层,用于放置内容主体、banner、feeds 流等
HUD 层:主要是各种交互按钮,用于触发主线动画、支线任务以及跳转其他页面
弹框层:有一层透明遮罩,放置各类弹框
Toast:最上层,反馈操作提示、loading等
可以看到大的分层已经有 5 层了,如果涉及到一些复杂动画,需要图层进行相互叠加,那层级关系可能会多达 10 +层。在多人参与开发的情况下,就会出现模块相互覆盖的问题。这里建议在团队内部进行一些简单的规范,例如:
动画层 z-index 范围在 1-100 之间;
HUD 层的 z-index 在 100-1000 之间
弹框层的 z-index 在 1000 以上。
这样,可以减少许多样式覆盖问题。如果使用 React 开发,还能通过 Portal 将弹框层和 toast 从主体内抽离出来,进一步减少冲突发生。
会场类页面,往往不会有较大额度的收益,而是跳转到商详等页面实现下单等功能。这些页面属于核心流程页面,有团队长期维护,十分稳定。而互动类往往可以在自身页面上完成提现、交易等高风险功能,且开发周期短,功能迭代快,往往一着不慎就会造成上千万的资产损失。
这里以抽奖为例。假设运营设置的金额上限是 100 元,后台下发了 2000 元的中奖金额,这个明显是一个错误数值,如果不做处理直接展示,用户就可以根据该截图进行索赔。当索赔人数过多时,资产损失就会非常大。当然如果下发一个负数,也会引起用户投诉。因此,我们要做一层金额校验,并通过友好的提示让用户去向客服咨询。
以下是我们的校验函数:
金额校验函数代码图
使用方法图
此外,为了防止出现拿截图冒充中奖用户的情况,建议在中奖页面显示防伪码,防伪码和用户一一对应。
相对于会场,互动中的模块没有那么直观和具体,一个看似反复出现的功能往往会贯穿若干个独立 UI 组件。这对如何沉淀模块提出了挑战。下面从实战出发,看看我们是如何抽丝剥茧,把复杂的常见功能沉淀为模块。
切换动效图
在互动业务中,弹窗是人机交互的核心。在分现金项目中,弹窗场景多达 54 个。如何应对数量众多且场景复杂的弹窗问题,兼顾准确性和高效率,成了必须要解决的问题。
我们在梳理过往互动业务后,对所遇到的弹窗场景进行了抽象、归因,将弹窗分为两类:
不需要用户交互,完全由数据驱动,当数据发生变化时触发的弹窗,我们称之为自动弹窗;
由用户交互触发的弹窗,我们称之为主动弹窗。
在分类完成后,我们就可以以弹窗触发方式为区分标准,将单个弹窗原子化,并抽象出两种通用的调用方式(action)。
我们还需要注意到,自动弹窗和手动弹窗并不是完全割裂的关系,而是可能存在链式调用;因此需要找到一种能够兼容二者特性的通用模型。
为了保证用户的体验,准确高效的将业务想要表达的信息传递给用户,弹窗的出现往往具有链式调用,即针对同一时间切片,只会触发一个弹窗;针对这个特性,我们采用数组来作为承载弹窗的数据结构,从而形成弹窗队列。
在业务需求中,通常会出现在已经触发弹窗的情况下,后续需要继续触发的弹窗的场景;不同弹窗之间的差异点在于触发时机和z-index层级。
针对这种场景,可以使用二维数组作为承载弹窗的数据结构进行优化,完成弹窗模块的模型抽象。
该二维数组可以覆盖以下业务中常见的 2 种场景:
二维数组的第一维对应不同层级的弹窗 UI
二维数组的第二维对应当前层级弹窗 UI 需要顺序展示的队列
针对第一维数组对应的弹窗 UI,每层弹窗 UI 永远展示对应弹窗队列中的第一个弹窗。
针对第二维数组对应的弹窗队列,自动触发的弹窗批量按顺序入队;用户交互触发的主动弹窗,通过一定方式,手动设置在弹窗队列的相应位置。
如此,在每一层弹窗 UI 上,都可以通过数据驱动的方式,当前一个弹窗结束后,自动的展示对应弹窗队列中的下一个弹窗;当用户操作弹窗后,通过相应规范方法,修改弹窗矩阵的数据,从而达到修改展示内容的目的。
弹框队列图
在完成对弹窗模块的建模后,下一步就是要制定弹窗模型逻辑层的操作规范,做到行为可追踪、方式可复用、结果可预测。
我们通过制定弹窗相关逻辑的规范,规范开发者调用弹窗的方式,以达到准确高效的实现业务弹窗逻辑的目的。
在我们的规范中,我们对能够修改弹窗模型的行为进行收口,抽象成独立的 action;与此同时,维护 action 和相关业务规则的映射;与此同时,实现一个能接并执行该映射的 runtime。
弹框矩阵代码图
在用户通过触发既定 action 时,runtime 开始工作;通过解析相关映射中的规则,runtime 判断该 action 在当前环境下是否能够执行,以及该 action 在当前环境下对应的实际行为;实际行为的执行结果,作用在弹窗模型上,修改了弹窗模型的数据;进而通过数据驱动的方式,达到更新 UI 的目的。
队列数据结构及触发流程图
相比以往散落在各个不同的时间切片上,命令式触发弹窗的方式,我们实现的弹窗模型有以下优势:
触发弹窗的时机统一整齐,方便管理和回溯;
不同层次的 UI 对应不同层级的数据结构,层级分明;
通过数据结构描述弹窗间的顺序关系,利用数据驱动的方式声明式的调整 UI,高稳定性;
逻辑与 UI 关注点分离,针对复杂逻辑及不同业务场景所需的定制逻辑,能够做到逻辑集中管理、维护,可拔插式支持定制逻辑,灵活高效。
用户进入游戏,通过邀请好友任务助力、浏览商品、逛店铺及会场等任务积累虚拟金币,达到一定额度后可进行微信或无门槛红包真实提现。作为互动游戏最终阶段,提现模块相对主线、支线玩法等需求,其内部核心逻辑较为固定。
然而现有开发模式存在以下问题:
不同的需求中产品、开发、测试整个流程重复进行。
提现内部隐含逻辑过多,开发过程中易考虑缺失。
开发成本高。
开发人员变动易导致系统稳定性差。
由于影响到用户利益,将其模块化以支持业务的高速迭代成为燃眉之急。
提现模块组件化需要结合需求,将提现逻辑分三个阶段进行划分,分析同与异。
618 叠蛋糕、11.11 全民营业等互动,到达兑换阶段后,主 button 展示为提现按钮,点击按钮唤起无门槛红包兑换或微信提现。而在分现金活动中,点击提现入口唤起提现面板,点击对应的渠道进行提现。提现面板展示对应渠道的状态、可提现金额、状态文案等内容。
提现界面图
在用户点击按钮进行提现之后,微信提现需进行 APP 版本判断、实名认证等逻辑处理,然后唤起微信获取提现 code。后续微信提现与红包提现一致,请求服务端接口,进行提现结果展示。在提现结果中,成功态存在提现明细弹窗(包含金额、日期、防伪码等)、文案弹窗(纯文字提示提现成功),失败态也存在 toast 或弹窗等状态。
提现弹框图
用户查看提现记录,展示提现记录:一展示单一提现记录,二分为微信或红包两类展示提现列表,列表根据日期做树形结构,可展开收起显示。
提现明细页
提现整体架构图
微信提现版本判断、获取异常 code、调用提现接口进行不同渠道提现逻辑固化,暴露提现方法。
待提现模块纯按钮形式可根据暴露的提现方法进行直接调用。通过渠道面板方式提现:封装提现渠道组件,外部传入调用提现核心处理方法,组件内部判断渠道进行处理。将组件UI与逻辑相结合开发。快速引入,根据参数实现高度可配置。
提现流程图
实行可配置化。业务可配置提现成功或失败最终结果展示样式,服务端下发即页面结构,以数据为基础,组件呈现不同的结果样式,提升可复用性。
提现数据结构化图
提现模块化,使开发人员不再需要重复开发提现逻辑与UI等,使用提供的提现核心hook与组件相结合,提升开发效率。在组件中,结合业务的常见提现方式及ui效果,实现高耦合性组件,用户只需配置参数,调用组件即可实现提现入口、提现明细、提现渠道至提现结果等整套功能。此举同样减少了整个项目流程中各节点的工作包括联调、测试等。更重要的是提高了系统稳定性。
基于包括本次城城分现金在内的大量互动项目开发经验,结合我们平时开发所使用的组件库和前端架构,我们构建了全新的互动开发引擎。
引擎框架图
我们把 CLI 定位为不只服务于互动框架,还支持团队内部其他框架的构建工具。
CLI工作流程图
使用 inquirer,CLI 支持选项选择。
将选项拼接成具体的模板,比如我选择的是互动框架,使用 typescript 和 sass 进行开发,那模板名称就叫 interact-template-with-typescript-and-sass,然后去下载该模板
下载的模板不是一个完整的项目,里面需要做一部分替换,我们使用 ejs 模板语法来完成这一步。
接下来,我们会输出模板建设规范,邀请外部团队共建生态。
我们将编译模块从 babel6+webpack3 组合升级到 babel7+webpack5,使用 thread-loader 开启多进程打包。实测对比,编译速度提升44%左右。
编译速度比较图
同时,使用 eslint+Prettier+husky+lint-staged 组合对代码规范进行了统一。
在上述基础设施之上,我们可以根据需求定制不同的模板。比如类似于分现金的互动项目,我们可以在模版内内置一些生命周期,提前注入弹框、提现等所需要的组件。后面再进行开发,只要使用该模版,即可获得大部分开发能力,减少约 30%的开发工时。
除了上述讲到的弹框队列、提现组件,我们还整合了团队这几年开发的各类业务组件,比如倒计时、loading、toast、任务组件等等,将其放在同一个 npm私有域中,以供公司内部使用。
我们将函数库划分为两类:
基础函数库,不依赖任何环境和外部包,可以供所有活动使用,比如上文提到的校验金额函数等。
业务函数库,业务中所需要使用的函数,可能依赖 APP、小程序环境,根据需求开发自己去选择。比如 APP 内跳转原生页面等。
想让更多的人使用这个函数库,文档必不可少。这次我们使用 typedoc(jsdoc 的 ts 版本)自动生成初步文档,并将文档改善后放入 vuepress 模板,使得开发者更容易上手。
技术文档截图
由于分现金小程序业务需要嵌入在京东赚赚小程序(以下称为:宿主环境)上,而宿主环境当时正由另一个小组开发中,变化比较频繁,如何搭建分现金小程序端开发的开发环境,成为一个棘手的难题。
在调研的过程中发现,京东赚赚有很多类似于分现金的活动需要接入(比如:东东萌宠、东东农场、领现金等),而大家较为常用的方式是拷贝(或 fork)宿主环境的代码,然后在宿主环境代码的特定目录下(例如:src/pages/)开发当前项目的。然而发现宿主环境(京东赚赚)项目有自己的工程化,也需要经过打包,才能产出在开发中工具总运行的小程序源码。这样一来有两种方式可选择,如下:
在宿主环境编译后的代码中开发当前环境:在开发当前项目时,可以减少宿主环境的打包;由于宿主环境打包会清理上次的内容,极易误删当前项目。
在宿主环境的源码中开发当前项目:这种方式相对较安全,并且还能享受宿主环境的工程化(比如支持 scss、npm 包等);但前提是先要把宿主环境的开发环境搭建起来,且需要理解其工程化的配置。
对比两种方式,不难发现,后者(在宿主环境的源码中开发当前项目)较优,开发方式如下图:
小程序开发模式图
虽然在宿主环境中开发方案可行,但是开发同学需要先搭建起宿主环境(京东赚赚)小程序的开发环境,以及去理解宿主环境的工程化的配置,无疑之中提高了开发的难度,分散了项目开发的关注点(除了关注自身业务开发,还需要关注宿主环境的搭建和配置)。
然而,通过仔细观察和分析发现,在宿主环境中开发的方案对于所有的业务开发来讲,都是一样的需要,并且这种合并方式都是遵循一定的规则的,试想是否可以将这些内容统一交给模块处理,暂不去关注模块具体内容及实现方式。
小程序开发模式图
这看上去跟前一张图并没有太大的区别,这一步封装的价值在哪里?带着这个疑问,再回归到实际的业务开发中,去观察其开发模式。在实际的开发中,一般各个业务都是独立开发,例如城城分现金和东东萌宠就是由两个小组开发的,并且两个业务有各自的推进节奏。因此,可以基于单个业务开发,进一步简化和抽象其开发模式。
抽象开发模式图
新的模式将宿主环境及其与当前项目的合并规则等功能都交由一个“透明层”(如上图 虚线部分)处理,之所以称为“透明层”,是因为其几乎对当前项目不可见,不再需要当前项目的开发同学关心如何去搭建“透明层”。如此一来,就可以解耦当前项目和宿主环境的开发模式,即开发同学无需在宿主环境中开发,甚至都不需要关心宿主环境的存在,而只需关注当前项目的业务开发,所有的宿主环境拉取和代码合并的问题,都交由这个”透明层“处理。因此,”透明层“必须包含宿主环境的代码,并且能够监听当前项目的文件变化,时时编译出最新代码。当然,由于”透明层“最终由一个 cli 编译器实现,因此还可以根据需要添加很多小特性,以提升开发效率,比如支持多环境配置切换、支持将预编译 less 的转化 wxss、更新宿主环境到最新代码等等。最终让我们来看一下“透明层”的功能方案图(即工程化方案图)。
工程化方案图
对于整个透明层,可以采用 cli 来承载。最终我们基于 Gulp 实现该 cli(以下称为 imi-cli),主任务(watch)负责监听当前文件的变化,并提供实时的编译,其主要职责如下:
watch 功能:监听当前项目的变化,如有变化,则重新编译
合并功能:根据入驻环境(京东赚赚)的规则,合并当前项目(分现金)的代码,并输出到当前项目(分现金)的 dist 目录(可通过参数修改)中。
less 预编译:支持 less 编译为 wxss
环境变量:支持多环境.env
在开发阶段时,使用微信开发者工具打开 dist 目录即可预览(或调试)整个小程序。同时,cli 还提供了 build 命令,只打包当前小程序的代码,以及 update 命令,用于更新宿主环境的代码。
在以往的业务中,开发人员通过框架结合基础组件(如toast、分享等)的模式进行需求开发。随着互动业务逻辑的日益复杂,相同业务模块追求细节变化,这使得原有的开发方式在沉淀业务模块方面较为乏力。我们使用模块和模式相结合的思想,构建了sirius互动开发引擎。基于引擎,我们可以沉淀实现某类需求的开发模式以及复杂的业务模块,如此可以把平时积累的技术知识沉淀下来,同时也能提高开发效率和稳定性。