达达集团技术

收银台前端重构实践

一、前言 二、收银台存在的问题 2.1 可用性方面 2.2 效率方面 三、可用性及效率解决方案 3.1 可用性建设 3.2 效率问题解决 四、收益及展望 4.1 收益 4.2 展望

一、前言

支付是交易达成的重要环节之一,京东到家产品中实现支付的途径是通过收银台系统完成的,收银台的重要性不言而喻。最近几个月,我们针对收银台项目中的存在的一些问题进行汇总,例如收银台支付方式的接入效率,项目维护成本高等问题,希望通过前端项目架构重新设计,来解决这些问题。

二、收银台存在的问题

纵观过去十几年,支付方式从最早的现金交易,后来的刷卡消费、到现在线上支付,支付方式在不停丰富和演进,但它一直在围绕着两个核心问题: 安全 和 效率 。针对京东到家收银台我们核心要解决的问题如下:

1、可用性方面

京东到家收银台H5版作为一个公共支付服务,支撑着达达集团30多项业务的支付功能。由于支付场景众多,容易出现遗漏测试场景,影响支付功能。

2、效率方面

新的支付方式接入,开发和测试成本较高,不能满足业务方快速上线的诉求。体现在以下几方面:
抽离问题
收银台H5版和收银台RN版,项目中组件没有进行抽离复用,导致组件维护成本高,例如收银台H5版和RN版实现同一个需求,项目层面需要改动不同组件,稍有不慎就会遗漏或改错。

层级问题

收银台入口组件比较大,组件行数达到1000+,组件没有做很好的分层,不同组件的逻辑都在入口组件文件中聚合,代码的可读性及维护性都不高,功能新增会使组件文件越来越大,越来越臃肿,组件熵增(如下图),导致问题排查时间长,bug率高。

设计问题

组件设计思考比较少,比如组件的内聚性不高,导致组件之间的耦合严重,造成组件的可维护性差,同时,组件的可测试性考虑不足,导致测试回归效率低。

三、 可用性及效率 解决方案

针对以上问题,将从收银台的可用性和效率两个维度进行方案的阐述。

1、可用性建设

(1)可用性方面问题原因主要是测试场景覆盖不全,所以在重构的过程中,对组件的可测试性进行了设计,为后续接入单元测试打下了良好的基础,自动化全覆盖场景测试成为可能。关于组件设计,下面章节会详细介绍。
( 2)除了代码质量,监控和报警,兜底和降级方案也是收银台需要时刻关注的点,例如由于paySource配置错误导致某种支付失败时,可以通过全局的公共兜底参数完成支付,保证支付功能的可用性。

2、效率问题解决

支付方式接入效率低问题的根本原因是项目自身的可扩展性比较差,重构设计方案中充分考虑了项目的扩展性和可维护性,将从以下这些方面进行介绍。

(1)组件复用

通过对收银台功能UI层面及现有组件进行梳理,明确了可以抽离组件的范围,下图中黄色区域内的组件都可以进行复用。

(2)组件分层

近些年,前端开发方式变迁如下:

  • 原生Javascript开发,直接通过 Javasc ript 原生api操作dom进行开发,已经开始注重分层(MVC),将数据请求和逻辑从HTML文件中进行分离,那时Node.js诞生没几年,工程化工具还没有现在这么丰富和强大,资源压缩丑化一般通过在线工具进行压缩。特点是开发需要对原生javascript api熟练掌握,工程化程度低。

  • 工具框架开始流行,尤其是jQuery,风光一时,基于jQuery的UI组件也是十分丰富,指令式和链式操作dom确实使代码更加简洁和有效率,几行代码就可以替换原来的大篇原生Javascript代码。后来,为了提升View层的开发,不用 Javascript 在中拼装字符串,开始出现各种Javascript Template库。特点是jQuery对原生 Javascript api 的封装和Template库的引入对前端的开发效率大大提升。

  • 随着模块化开发和前端工程化和的发展,国内出现了第一批优秀的打包工具,例如fis等,再后来Webpack出现,因其更好的插件扩展机制及生态,用户量越来越多。通过这些工程化工具可以进行优化配置,使项目的优化更加简易,使开发者更加专注业务的开发。

  • 组件化开发时代的到来(Angular,Vue和React等),通过声明式标签进行分层组合,完成View层的搭建,开发者只需要考虑Model的设计,及更新时机。View层的数据更新,框架底层会优化渲染。组件化开发的特点就是Ui的展现都可以清晰的,可预测性的描述出来,比较利于维护和测试。组件化开发可以像积木一样进行拆分和组装。

通过对前端开发方式的梳理,大家可以了解到各个阶段前端开发的一些特点,从而更好的理解现阶段前端组件化开发的特点。组件化开发思维就是组合思维,怎么让组件能够有序的组合到一块,就需要对组件进行分层设计,通过 分层 达到 分治 的目的。在介绍组件分层之前,先明确组件的分类:
  • 基础组件 是指和业务无关的组件,例如基础的UI组件(DJText,DJImage,图片上传组件等)。

  • 业务组件 就是实现具体业务UI的组件,例如项目中CashierPayTypeItem组件(收银台支付方式组件),包含了具体业务属性。

组件分层如下图:

分层逻辑:

  • 基础组件层 代表着基础组件这一类组件。

  • 业务组件层 代表着业务组件这一类组件,业务组件层和基础组件层关系如下图: 示例代码如下(react):

    <View style={styles.bankBox} >    <DJTag        style={styles.bankTag}        type={'emptyTag'}    />    <DJImage style={styles.logoUrl} source={item?.logoUrl} />    <DJText style={[styles.mainTitle]}>        {item?.mainTitle && item?.mainDesc ? `${item?.mainTitle},${item?.mainDesc}` : item?.mainTitle}    </DJText>    {ifShowArrow && <DJIconArrow size={13}  style={styles.iconArrow} />}</View>
  • 业务粘合层 业务粘合层就是组件的控制层,负责组件的调度,向业务组件或基础组件提供数据和行为逻辑处理方法(事件回调)。层级关系如下图: 示例代码如下(react):

    <View style={[styles.container, isLast && styles.isLast]}>    <View style={styles.wrap}>        <DJImage source={payMode?.iconUrl} style={styles.iconUrl} />        <View style={styles.box}>            <ItemHeader payMode={payMode} checked={checked} source={source}></ItemHeader>            {/* 主活动文案 */}            <ItemText payMode={payMode} />            {/* 微信免密开通 */}            {payMode?.payMode == 10 && this.state.showNoPass && <ItemNoSecrect payMode={payMode} applyContract={applyContract}/>}            {/* 银行活动文案 */}            <ItemActivityText source={source} argsData={argsData} payMode={payMode} token={token} cashierData={cashierData} selectedPayMode={selectedPayMode} onPressEv={this.onPressEv} />        </View>    </View></View
  • 页面逻辑层 一个页面只有一个页面逻辑层,负责对其他层的控制和调度,可以这么理解页面逻辑层是最顶层的业务粘合层。层级关系如下图:

    虽然将页面的组件分成这四层,但并不表示页面组件只有四层,组件的分层是一个树。收银台页面分层示意图如下:
    组件分层对应的数据流向处理

  • 业务粘合层负责和状态管理器(redux|mobx)通信,负责将数据和事件通过props传递给展示型组件(展示型组件是指没有任何绑定及依赖的组件,通常是无状态组件,本文中的基础组件和业务组件理想状态下都是展示型组件)。

  • 展示型组件可以通过事件触发上层传递回调来改变状态管理数据,达到数据的更新。

  • 展示型组件只负责数据展现和回调注册,不直接和状态管理器通信,可以保证组件的复用性,正交性和可测试性。

    新增业务粘合层的条件
  • 组件中的props不是自己使用,而是传递给子孙级使用,就需要考虑添加业务粘合层来进行分层处理了,避免每次子级新增数据需要逐层添加。

  • 如果页面逻辑层的子级业务组件超过10个,就可以考虑添加业务粘合层,比如老的收银台入口组件,由于没有添加业务粘合层,代码行数1000+,组件的可读性和维护性都比较差。可以根据页面布局和功能的特点,将逻辑层拆分成若干粘合组件。分层后,组件结构清晰,代码可读性增强。

    组件分层的注意事项
  • 在尝试将组件分层的初期,需要一些学习成本,例如需要识别分层条件,同时还需要对Model进行设计,让数据和粘合层更好的搭配。

  • 分层不易过深,建议嵌套不超过三层,超过三层后数据处理就会变的复杂。

  • 组件化分层没有终点,分层不是一成不变的,例如业务更新迭代,交互复杂度变高,原来的分层已经不能满足现在业务,那就需要继续分层,减少组件熵增。

(3)组件设计原则

组件开发思维就是组合思维,要实现组件的高内聚和低耦合的目标,设计组件的时候就需要遵循以下原则:
  • 单一职责原则 (SRP:Single responsibility principle)又称单一功能原则,它规定一个类应该只有一个发生变化的原因。对于组件设计的指导就是组件不要承担过多的职责,避免修改组件内单个功能的代码,影响了组件内的其他功能。因为组件足够原子,组件的可组合性也随之提高,具体的操作可以参考以下方式: (1)单个组件文件行数建议不要超过200行。 (2)组件参数不要过多,尽量控制在4个以内。

  • 迪米特法则 (Law of Demeter)又叫作最少知识原则(The Least Knowledge Principle),一个类对于其他类知道的越少越好。对于组件设计就是减少组件之间的耦合,避免组件和外部环境的关联,使组件的输入和输出可以保持一致,组件的可测试性比较高。具体的实践操作如下: (1)最好使用函数组件,追求纯组件。 (2)尽量不要暴露组件的引用,避免直接操作dom。 (3)组件内部不要操作修改外部变量,避免影响其他组件。

(4)逻辑抽离

逻辑抽离示意

收银台逻辑的抽离主要是各个端的支付方式逻辑抽离,抽离示意如下: 从上图可以看出,对不同的支付方式从组件逻辑中进行了抽离,可维护性更好,同时不会因为重构使RN包的文件变大。支付回调的抽离方式和支付方式类似。

抽离逻辑设计
逻辑抽离后需要考虑怎么合理运用设计模式使新增的支付方式能够快速的接入,并且不会影响原有的支付方式。支付方式逻辑抽离采用了策略模式,伪代码实现如下:
// 入口函数 index.js/** * @param {Object} payData 支付完成必须参数 * @param {Object} payMode 支付类型 * @param {Object} currentPayMode 找人代付参数 * @param {Object} successCallBack 支付完成回调 * @param {Object} failCallBack 支付失败回调 * description: 支付封装SDK * */import payType form './payType/index.js'// 采用策略落实,易于扩展维护export default toPay(dataObj) {    payType[dataObj.payMode](dataObj);}// payType 入口函数 index.jsimport JdPay from './JdPay';import wxPay from './WxPay';import CloudPay from './CloudPay';import DaiPay from './DaiPay';const payTypeObj = {    '10': wxPay,    '20': jdPay,    ...}export default payTypeObj;// JdPay 的入口函数 index.js  和 index.web.js// index.js RN的京东支付逻辑export default function jdPay (dataObj) {    const PayData = {            type: 'thirdPay',            param: data,            returnType: 'callJS',            success: data => {                dataObj.successCallBack(data)        };        JDPaySDK.jdPay(PayData);}// index.web.js H5的京东支付

四、收益及展望

收益

  • 重构后,收银台的可用性持续增强,良好的组件设计,让组件的可测试性提高,新增需求的bug率明显降低。重构后的组件为后面逐步接入单元测试打下了良好的基础。

  • 重构后的收银台对于新支付方式的接入更加容易,更加有效率,重构完成后收银台的支付方式的接入工作量从原来的最少3天/人,减少到现在的1 天/人,能够更快的产生业务价值。

  • 收银台组件层和逻辑层的代码抽离复用,符合预期效果,包文件大小比重构前包减少20KB,约占重构前的10%。页面文件大小的减少对RN包的升级或者H5资源的加载都是有益的。

  • 组件分层和组件设计规范在团队的沉淀。通过组件分层和组件设计标准在收银台重构的实践,让大家切身体会到组件分层对项目维护性和扩展性的提高。组件的设计规范让大家意识到要想抽离一个高内聚,低耦合组件需要注意哪些原则。

  • 善用一些设计模式,可以让你的代码的维护性和扩展性增强。通过对设计模式在特定场景下的练习,可以提高你对设计模式的使用和理解。

展望

为了进一步提高收银台的可用性,后续还会通过以下几方面进行完善:

  • 项目层面引入TS。 由于TS的强类型特性,在编程阶段,TS就可以发现项目中的很多空指针问题,让问题提前暴露,增加项目线上稳定性。

  • 组件单元测试全覆盖。 收银台测试场景用例的持续补充和完善,减少测试场景遗漏,提高上线前的回归效率。

  • 报警机制添加。 在项目APM方案的基础上,我们将继续添加报警策略,让功能的不可用可以提前感知,使研发快速介入,减少支付不可用带来的损失。