背景
订单是电商服务的核心场景之一,微商城客户端的订单模块已经服务了商家多年,功能和体验上和 PC 端有一定的差距。为了弥补不足,提升商家的体验,产品经过一系列数据调研,发起了微商城订单模块的重构项目。
作为“乐于重构”的开发者,在此次重构中以增强代码维护性以及线上稳定性为目的,接受了这次挑战。接下来将从业务代码架构、历史代码改造两方面,简单地聊一聊我们在此次重构中的一些经验。
业务代码架构的改进
1. 组件拆分
上图为旧订单列表和新订单列表的截图
上图是新订单列表中订单状态配置和筛选项配置的截图
不论是新订单列表还是旧订单列表,页面核心功能区域 UI 均分为订单状态、订单类型、及订单卡片列表三个部分。这部分基础的逻辑并没有变更,但是每个部分的可选项增多了,灵活性增加了,在旧订单列表上进行变更代价较大。
在代码逻辑方面:
Android 侧订单列表过去的多个列表入口均继承自 AbsTradesListFragment
,具体继承关系可见下图
各个实现类的页面只是提供了不同的网络请求参数,这种设计好处是对于订单的通用变更,只需要改 AbsTradesListFragment
即可。但缺点也很明显:由于每次变更范围是订单列表,修改时往往又是局部变更,每次需求回归时都需要重新回归整个各个影响,为了减小 UI 变更影响的范围,只能在 AbsTradesListFragment
里写上各种判断逻辑,所以维护成本比较高。同时由于所有业务都堆在AbsTradesListFragment
内,导致代码行数已经达到了 1000 行,这给开发人员维护老的订单逻辑也带来了不少的问题。
为了在新订单列表重构的过程中,尽可能的规避掉旧订单列表中相关的坑点,Android 侧将订单列表页面从不同的维度进行了拆分:
从 UI 层面
新的订单列表将订单状态选择器、订单类型选择面板、订单卡片分别拆分成了不同的视图,每个视图仅负责相关的选择内容的输入输出,所有业务无关的操作逻辑均对外均不可见,以求组件逻辑变动对外部影响最小化。
与此同时,将订单卡片中各个子内容封装为控件,以便于卡片视图内部对各个子组件来对订单视图进行组合,来支持多种样式的订单卡片。在这种设计的方案中,后续订单卡片新增或修改某些状态的 UI,只需要变更卡片样式即可达到 app 内所有订单页面生效。
从架构层面
摒弃了之前老的订单页面中将数据操作和界面变更堆叠在
AbsTradesListFragment
的设计逻辑,使用 view model 来完成网络请求的处理,订单列表 UI 层只负责各组件间数据的交互及网络请求结果数据的展示,规避掉之前旧订单列表中极端场景下网络请求和页面声明周期冲突导致页面不展示订单数据的问题。与此同时,订单列表中每个状态 tab 都会进行预加载的逻辑,每个状态 tab 对应的 fragment 都提供单独的数据源,给用户更加直观的速度提升的感受。为了适应后续可能会变更和新增的订单状态及订单类型,订单的初始化参数以
Map
的形式传入新订单列表,在订单列表内对不同参数做对应处理,减少后续变更对 app 页面逻辑的改动。
2. 路由切换
解决的问题:iOS(组件间切换),Android(组件、页面间切换)。
微商城移动端的订单列表重构项目,产品的需求、设计、交互、数据结构,相比过去的旧订单列表有很大的差异,如果直接在旧项目上进行修改,会有一些需要考虑的问题:
- 订单列表是微商城客户端的核心使用场景之一,直接全量发布难以保证线上稳定性
- 客户端的列表模块代码历史较长,从产品经理、设计师到开发,经手人都比较多,难以保证新的设计可以覆盖到所有的使用场景
- 订单列表模块,从设计上已经拆分为正向订单和逆向订单(维权、退款订单)两个类别,对于新的组件,旧的路由设计存在局限性
针对前两个问题,我们希望新订单模块可以灰度上线,在确保不影响商家正常使用的前提下,逐步取代旧订单模块,如此,我们真的只需要重写改业务的客户端模块,共存上线即可。
针对第三个问题,我们希望新的路由可以抛开旧路由格式的局限,更具扩展性,但也要顾及模块共存时路由的分发处理。那么就需要将目前模块间的路由跳转进行统一管理,以便对新老订单列表随时进行切换,所以,我们在项目中使用了动态路由组件:
上图中, KDOrderViewController
是旧订单列表的入口, wsc_orderLoca.YZOrderViewControl
是新订单列表的入口。 wsc://order/orderlist
路由,在做分发时,经过了动态路由组件 DynamicRouterDispatch
进行分发处理,根据灰度规则,映射新的路由 wsc://order/v2/list
进行跳转。如此,对于旧订单组件,我们几乎没有做修改,只需将路由的分发进行兼容处理即可,这也是组件使用路由跳转的优势。
对于附带参数的路由,我们处理时依照类似的规则:
wsc://order/v2/list?buyer_id=$1&fans_type=$2
使用正则表达式,将旧列表的参数,提取、转换为新的路由,就能够完成路由的兼容。
3. 路由降级
在订单重构的实践中,最初在考虑灰度方案时,仅以比例灰度为目标,所以可以直接通过配置中心的灰度即可。但后期,希望有店铺白名单的支持,那么就不能以动态路由的配置灰度来完成。于是,动态路由也做了一些迭代。
- 微商城的动态路由配置版本号,跟随客户端版本号
低版本客户端不存在新订单模块,无法支持新路由,未来也可能会存在其它不兼容的新路由;客户端版本号约束所有业务版本,可以保证跨业务的路由的一致性 - 路由组件自身增加降级处理,在跳转不到新路由时,回退到旧路由
- 动态路由规则添加备选规则
这一点主要是解决,当路由本身并不是一个有效路由的情况。这种情况,在微商城客户端是存在的。目前的应用列表下发的不一定是有效的路由,如果不是,那么 2 就需要一个备选规则来进行“回退”操作 那么一个订单管理的入口路由:
^wsc://trade/order/list$
对应的规则类似如此:
{
"template": "wsc://order/v2/list",
"alternative-templates": [
{
"alias": "orderListV1",
"template": "wsc://order/orderlist"
}
]
}
名为 orderListV1
的路由模板,会在路由进行降级时作为备选规则进行跳转。那么,路由的灰度、白名单命中过程,就是这样的:
历史代码改造
1. 订单操作处理分发
过去的订单操作处理分发存在的问题:
- 过去由于订单列表没有拆分处理,所以有一部分订单卡片操作写在
AbsTradesListFragment
内,另一部分写在订单卡片对应的列表的viewHolder
中,这就无形中增加了开发人员的维护成本,每次进行变更时总要先查下变更的操作写在哪个地方。
如左侧图所示,订单权限校验分布在不同的文件中,变更时开发者需要排查自己是否遗漏了逻辑。
- 在过去的订单列表中对对应卡片操作后订单列表没有感知处理,从而无法更新具体的订单的 item 的数据。每个业务方在对订单执行操作(如发货、退款)后,订单列表页面的内容展示不会变更。
在新的订单列表中:
在新的订单列表中,Android 侧的订单列表将对应的卡片的点击操作处理交给了订单卡片中对应的子组件进行处理,业务方添加订单卡片操作处理时只需要关注对应组件即可。
为了简化订单列表刷新的流程,在新订单列表的 Fragment 中增加了对订单状态刷新的事件,业务方在需要刷新对应订单状态时,只需要发送对应的事件,不需要关注订单所在的页面和订单的状态,即可完成对订单列表的刷新。
- 新订单列表中将权限相关的逻辑与对应订单操作处理统一在一起,防止操作逻辑变更时遗漏掉权限处理。
2. 卡片设计
由于旧订单卡片不同订单展示样式一致,所以在平时开发迭代时,考虑到成本,对于订单卡片的样式一般不进行变动。在新的订单卡片中,为了将订单卡片样式设置更为灵活,我们将新订单的订单卡片进行了纵向拆分,将订单卡片一共拆分为如下几个大部分:
- 顶栏区域
- 客户信息区域
- 配送状态区域
- 商品信息区域(包含折叠区域)
- 费用展示区域
- 操作按钮区域
- 同城配送状态展示区域
如此,一个订单卡片可以根据自身的数据,构造出不同的区域,而每一个区域均有独立的 view model 和 view 一一对应。这样做的好处:
- 拆分职责,避免臃肿复杂的模型、视图的产生
- 容易扩展,后续如有新功能区域,只需继续堆叠视图、模型即可
每部分均为独立组件,不同的订单样式上的差异展示只需要对对应的组件做设置控制样式,进行显示隐藏即可适配不同订单类型状态。在这种场景下 UI 方面的小改动往往只需要调整对应小组件的展示即可,增删改均能比较快速的支持。
总结
对于订单模块这种业务逻辑比较复杂的模块,我们可以将整个页面拆分成不同的组件,对于不同类型的执行逻辑尽量收缩到一个较小的影响范围,以便于变更扩展时影响范围可控。同时在开发重要模块功能时也要准备好充分的降级方案与灰度方案,来将新功能上线带来的问题影响范围尽可能缩小。每个基础的SDK看起来功能相对较为单一,但是组合起来就能达到不错的效果。