凹凸技术揭秘:如何服务 toG 项目——数字人民币项目前端总结
前言
toG 项目——一个在我等日常工作中极为罕见、极为神秘的项目领域,所有经历过的人,都仿佛经受了一场狂风暴雨的洗礼,谁做谁知道。
而数字人民币项目,光看名字就令人心生敬畏——新的货币形式、政府合作项目,充满着未知与挑战。事实也的确证明,这一不同寻常的项目,拥有从政府侧沟通、产品策划、业务投放、交互视觉设计、素材制作、前后端研发、测试到项目演练的长链路流程。而在这个流程中,波涛起伏的挑战扑面而来。整个项目团队从一开始措手不及的慌乱萌新,也渐渐被现实鞭挞成了兵来将挡水来土掩的铜墙铁壁。
那么这到底是个怎样的项目,而作为前端的我们又是如何应对的,本文将细细道来。
目录
- 项目特点
- 多金
- 紧急
- 善变
- 快速交付的前提
- 梳理活动状态流:透过现象看本质
- 聚合活动数据流:中心化管理数据
- 按需解耦:UI 复制,逻辑复用
- 风险控制的措施
- 减少风险数量:对发版下手
- 降低风险转变故障的概率:只要跑得够快,bug 就追不上我
- 减少故障影响范围:模块划分的奥义
- 添加模块加载器
- 定义合适的部署层次
- 缩短故障影响时长:把客诉扼杀在摇篮里
- 结语
项目特点
截止文章发布时,京东与地方政府协作的数字人民币项目,已完成了苏州、北京及成都三地的报名、抽签及发放全流程。
本文中描述的数字人民币项目,为承接了京东主站 APP 中报名、中签查询流程的 H5 页面。
本次项目的主要特点,可以用三个词概括:多金、紧急、善变。
多金
本次活动的预算高、单用户收益高,每个中签用户可获得200(成都可高达238)人民币的赤裸裸收益,根据以往的经验,高收益代表着高客诉风险,因此在活动流程及页面内容展示方面必须做到极度严谨、毫无破绽,要求项目各角色具备极高的风险意识。
紧急
项目初期疯狂压缩的排期,对于项目既快速又高质的落地提出了巨大的挑战。同时,在临近政府交付期限,以及演练的过程中,常常出现不可抗的需求调整,且留给项目组的时间常常只有1到2天,如何安全、快速地迭代,又是一大考验。
善变
在项目推进的1个多月中,大规模的需求变更总计达到了6次,其中有一次甚至砍掉了一大半的业务玩法,每一次的变动,都将给项目带来不可预知的风险。如何在每次变动的过程中——尤其在活动进行时——尽量降低项目风险,也是需要充分考虑的。
这三个项目特点,转化到前端肩上的挑战,就是如何取得项目快速交付与风险控制的平衡。
快速交付的前提
前端实现项目快速交付的前提,也是许多活动开发常常忽略或者略过的前期阶段——详细的需求分析与架构设计。这一类短周期活动,在活动开发时对于架构设计必要性的要求并不高,一般只要满足了功能、通过了测试,在一两周的周期内不出现线上问题,就完成了这个项目的使命。至于架构设计的合理性与可扩展性,看起来并没有那么重要。
但在这样一个快速迭代、流程复杂、随时有可能调配人手协作的项目里,前期的准备就显得尤为重要了。
梳理活动状态流:透过现象看本质
通过数字人民币整体交互稿可知,整个活动的交互流程包含了用户报名环节、审核环节、中签结果查询环节以及线下站外扫码环节几个阶段,整个活动流程非常冗长且状态十分复杂。
因此在进行活动具体逻辑的开发之前,第一步做的是梳理整个活动的状态流,将交互流程中那些存在分叉和相交的流程节点进行梳理和整合。
举个例子,数字人民币项目的交互流程中,每个阶段都存在一些异常的状态,但实际上活动的核心流程中不应该包含这些异常的状态流,因此在梳理过程中,我们将异常的状态流抽离了出来,将这些异常状态放在了其他的地方,如请求函数中,进行单独的判断,而核心流程只保留了诸如报名、审核和中签等关键状态。
这一步骤自始至终的目标,都是使得最后呈现在代码中的状态流能尽量的简洁和清晰,这样在之后进入具体逻辑的开发时,可以不需要再花太多的精力聚焦在诸如“当前是什么样的流程和状态”和“会不会有其他的状态与当前状态是相交或者重复的”的问题上面了。
聚合活动数据流:中心化管理数据
在本次数字人民币项目代码中,我们将页面中所有核心数据都存放在最顶层的 Mobx 中进行统一管理,自上而下地或是直接地将数据分发至各组件中。
另外,需要避免子组件通过回调函数直接更改父组件的状态或者数据,更推荐的做法是通过 action 更改 Mobx 中存放的状态字段,通过 Mobx 自上而下地对需要更新的组件进行状态的更新。
可以通过下图看到,没有对数据流进行聚合前,组件间的数据传递是混乱的、无章法的,这明显不利于对项目代码后续的迭代和维护。
通过数据流的聚合,可以很为方便地在代码层面梳理清楚整个页面的数据流走向,数据从哪里来,到哪里去,又产生了什么影响,也就十分清晰了。
按需解耦:UI 复制,逻辑复用
在软件开发和设计中,有一个很有名的思想“复制优于复用”,引用《“内源” over 中台》中的解释,即“追求复用会加深系统间的耦合度。在系统重构时,一种常见解决方式就是复制,在开源世界里就是:方向不同,即可在其基础上另起炉灶(fork)”,在数字人民币项目中的 UI 层,我们也希望遵循同样的思想,讲究复制,而非复用。
“复制”的实质,其实是“解耦”。将相同类型不同样式的UI结构代码耦合在一个组件中,其实并不利于后续的维护和快速迭代。
在数字人民币项目中,我们遵循的原则是“UI 复制,逻辑复用”,即相同类型的 UI 结构我们更倾向于另起炉灶,不同样式的 UI 结构对应不同的组件文件,最后再通过模块加载器将这些相同类型不同样式的 UI 组件分发出去。并提取不同 UI 组件中的相同逻辑代码,做到最大程度的逻辑复用。具体的做法在下文中会有详细的描述。
风险控制的措施
在风险控制方面,我们主要从减少风险数量、降低风险转变故障的概率和减少故障影响范围三个方面来思考和实践,以保证活动页面的高可用。《高可用的本质》中提出了风险期望公式,如下图所示:
文章《高可用的本质》提到,在上述风险期望公式中,控制风险主要从 nPRT 四个方向去考虑,nPRT分别对应『减少风险数量,n』、『降低风险变故障的概率,P』、『减小故障影响范围,R』和『缩短故障影响时长,T』,我将在下面把其中的几个方向与数字人民币项目中相关实践相结合,来阐述我们在本次活动中是如何通过控制风险来降低线上出现故障问题的可能性的。
减少风险数量:对发版下手
《高可用的本质》中提出,“所有事物都不是100%可靠的”,不管是人还是机器,都有犯错的可能——开发会因为粗心写错代码,软件会因为系统BUG导致系统故障,硬件机器会因为耐久度和异常环境导致功能异常和损坏。而从需求变动到活动上线,我们的代码经历的开发、编译、打包、部署的流程中,都存在着人和机器犯错的可能。《高可用的本质》中还提到,“从概率学角度分析,凡是有可能会出错的,只要变化次数足够多,最终出错的概率会无限趋向于1”,结合文中的风险期望公式可知,需求变动的次数越多,风险期望就越高。
日常项目中,频繁发版无疑将增加风险数量。
在此次项目中,我们考虑从规范提需流程,隔离运营数据与逻辑入手,以最大程度降低发版次数。
一、规范提需流程
在需求提出时,严格评估需求变动的必要性和风险;需求确认时,与项目各角色同步信息,并通过邮件等方式进行统一的记录。提高需求变动的门槛,过滤重要性较低的需求,降低需求变动频率。这也是所有大型项目应该遵循的规范化流程。
二、隔离运营数据与逻辑
首先明确一下两个名词的含义。运营数据指项目中与业务文案、视觉素材等相关的数据,逻辑指页面主流程、交互相关的代码文件。逻辑的部署,会经过编译、打包、上传、发布几个步骤,每发布一次,都存在的一定的发版风险。
本次数字人民币活动中,通过运营数据与逻辑通过两个独立的平台进行管理,来最大化减少逻辑发版次数。
运营数据管理方面,通过接入PPMS(运营内容管理平台)系统,将页面中可预见修改的模块内容来源进行了可配置化处理。代码上线后,如遇到了突发的非核心流程逻辑的需求变动,可通过更改配置项更新数据源来实现页面展示内容的变动,而无需重新部署逻辑代码,大大降低了运营内容频繁的变动而可能引发的部署风险。
降低风险转变故障的概率:只要跑得够快,bug 就追不上我
活动页面存在风险,是个无法避免的问题,但存在风险不代表着上线后一定会发生故障,只是存在出现故障的可能。为了在上线前对已知的可控的风险项进行排查和检测,可制定一份上线前 checklist。
数字人民币项目的 checklist 包含两类内容,一类是通用的检查项,一类是项目特有的检查项,最大化覆盖可预见的风险点,解放开发心智专注开发。
减少故障影响范围:模块划分的奥义
减少故障影响范围,最主要的手段就是对逻辑进行模块化隔离。
在此项目中,主要从两个方面着手:代码层面的模块隔离,以及部署层面的模块隔离。
添加模块加载器
在模块设计层面,我们添加了模块加载器,对相同类型的模块进行了统一的收集和分发,将这些模块进行了完全的解耦和隔离。 以往在进行模块开发时,我们通常会有思维惯性,习惯性地从局部去思考和设计模块的使用,且常常忽略重构的重要性。
我们来看一个例子,当我们创建一个头部组件时,第一时间会想到以下的实现方式:
class Header extends React.Component {
render () {
return (
<div className='header'>I am header</div>
)
}
}
目前看似一切正常,但不幸的是,需求往往是反复多变的,假设有一天,这个头部组件需要根据不同的状态增加一个商品的展示,那么很自然地,代码将会变成以下的模样:
class Header extends React.Component {
render () {
const { isGoodShow } = this.props
return (
<div className='header'>
I am header
{
isGoodShow ? (
<div className='good'>
<img className='good-url' src='url' />
<span className='good-title'>头部商品</span>
</div>
) : null
}
</div>
)
}
}
我们可以看到,我们将头部商品展示的逻辑,直接写在了唯一的头部组件中,由于当前的组件并不复杂,所以看似代码逻辑十分清晰明了,并无任何问题。
但试想一下,当头部组件中需要继续根据不同条件出现其他各种各样的模块时,整个头部组件会冗杂着这些模块的所有逻辑。 在未来快速的迭代和需求变更中,我们有可能需要对头部组件中某个模块的代码进行更改和调整,在这个时候,面对冗杂着所有模块代码的头部组件,调整代码导致出错的概率将大大提高。
在数字人民币项目中的情况就是如此,在项目中同样存在头部组件,头部组件存在着未登录、未开启定位、未报名、审核中、已中签和未中签六种情况,如果我们将六种情况都耦合在一个头部组件中,显然是不合适的。
计算机领域有句名言:“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”, 因此在数字人民币的开发过程中,我们对头部组件以及存在类似情况的模块组件都进行了一次重构,在组件的调用层和组件间建立了一层中间件,我们通俗地称它为“模块加载器”。
模块加载器的职责很简单,收集所有的头部组件,并将他们根据不同的情况进行分发,我们来看下面一段代码:
import BaseHeader from './base'
class HeaderA extends React.Component { /***/ }
class HeaderB extends React.Component { /***/ }
class HeaderC extends React.Component { /***/ }
class HeaderModuleLoader extends React.Component {
render () {
let Content
swtich (headerType) {
case 'a':
Content = <HeaderA />
break
case 'b':
Content = <HeaderB />
break
default:
return <HeaderC />
}
return (
<div className='header'>
<BaseHeader />
{Content}
</div>
)
}
}
在上面这段代码中,我们很好地对头部组件进行了分类,将它们可复用的部分提取在了BaseHeader
组件中,而其他不同情况下展示的内容对应地封装在了不同的头部组件中,通过switch...case...
进行对应情况的分发。
上面这段代码,本质上是策略模式的一种应用,HeaderModuleLoader
模块加载器会返回不同的内容,但它们都是属于header中的一种,只不过我们在模块加载器中进行了不同的策略判断和分发。
经过这种模式的处理,我们便成功地对不同情况的头部组件进行了解耦,当之后再出现需要调整或更改某个头部组件模块时,我们只需去到对应HeaderA
或者HeaderB
中进行对应的调整即可,也不会影响整个头部组件的功能和使用。
同时,我们还对项目中其他拥有相同情况的模块进行了一并的改造。在之后数字人民币的迭代和调整中,出现过很多类似“审核状态下的头部,我想加一段文字”的需求变动,在以前,我们需要在唯一的头部组件中,去通过条件语句判断活动状态来展示新增或是删减的内容。
但在经过改造后,我们只需要进到对应状态的头部组件去新增或者删减代码即可,不用再去写任何多余的判断,也不需要考虑当前的修改会不会影响到其他的内容和逻辑,可见,这种设计模式给我们带来了极大的开发便利和效率提升。
总的来说,为了前端模块未来的可拓展性和可维护性,我们需要对这些频繁变更模块的UI层进行更彻底的解耦,对逻辑层进行更彻底的复用。
定义合适的部署层次
秉承着鸡蛋永远不放一个篮子里,大树也永远不抱死一棵的理念,在这次数字人民币活动中,针对部署隔离,我们主要做了两件事:运营数据与逻辑部署的隔离,及页面级部署的隔离。
运营数据与逻辑部署的隔离,已在「减少风险数量」一节中有了详细的说明,此处不再赘述。
页面级部署隔离方面,我们参考微前端思路,将一些和主流程关系不大的流程和模块进行了页面层级的抽离,将原本唯一的活动页拆分成了站内H5页、站外H5页、数字人民币APP下载页以及异常情况处理页4个部分。页面间利用URL查询参数进行通讯。每个页面都进行单独的开发和部署上线。
而这样定义部署层次的原因主要是因为以下两点:
- 从源码层面来看,这样做主要是为了保证主流程页面逻辑的干净和简练,主流程页面的代码是整个活动的核心,将越多无关紧要的细枝末节进行剔除和隔离,越能保证主流程逻辑调理的清晰和干净,一定程度上提高了主流程代码的稳定性和可读性。
- 从风险层面来看,将一个活动页拆分成四个是为了将风险分散,打散后,不管站外H5出现了任何问题,都无法对核心流程的站内H5产生影响,反过来也是同理。这和投资理财要做好组合选择是同个道理,做好风险对冲,是控制风险中重要的一环。
缩短故障影响时长:把客诉扼杀在摇篮里
很多时候,一个项目,经过完整测试用例的捶打还远远不够,有些隐藏的问题,不经过大流量的冲刷,是难以显现的。例如此次项目,在演练过程中出现的,『活动太火爆』界面出现概率超出预期,是通过少量账号及少量访问量难以暴露的问题。
产品&业务同学:“有用户反馈说页面跳转到太火爆了,是接口有异常情况吗?”
后端同学:“后端监控这边没有看到有异常的数据,有可能是前端这边请求超时了。”
产品&业务同学:“前端同学这边看看是不是有这个问题。”
前端同学:“额……前端没办法看到有没有这个情况。”
线上监控的接入,则可以在一定程度上提早发现这一类问题,并提前进行相应的处理,及时止损,与客诉赛跑,甚至把客诉扼杀在摇篮中。
而此次项目中,由于工期限制,前端监控并没有做到妥善的规划与接入。在项目后期有时间接入时,又遭遇项目代码迭代安全性风险,及测试成本的挑战。在项目开发规范中对于接入监控的非强制性,以及监控接入的时间与心智成本较高,是根本原因所在。
团队已在京东商城 PC 首页项目中产出了最佳实践:接口可用性监控、测速监控以及代码异常监控相结合。通过接口可用性监控,一旦出现接口低可用率或者异常调用量的情况,通过电话以及消息推送的方式对开发者进行实时提醒。而测速监控则对于页面及接口的性能优劣起着监控作用。代码异常监控可上报页面的报错信息,由开发者进行筛选和甄别,判断有没有出现开发时没有被排查到的线上问题。
基于最佳实践,考虑为开发者提供一个可以快速接入监控系统的 SDK,以此来解决『如何快速地接入这些监控系统』的问题。将一些必要的接入操作整合在一套 SDK 中,并对监控系统原本提供的一些业务方法进行拓展,使其更灵活,适用更多的业务场景。
结语
正如开头所说,本次前端侧的挑战在于如何取得项目快速交付与风险控制的平衡,本次项目中我们根据以往的大促经验及理论进行了针对性的思考与实践,基本实现了把风险控制在了可控范围内。但目前所有的措施还是依赖于执行人的人工操作以及主观意识,我们会基于此持续探索优化,以全面接入数字化平台流程为目标,最大化进行风险管控,以更好的持续性地对 T 级互动进行支持。