小程序订单列表重构解决方案
一. 各业务订单列表页面地址(历史)
业务模块 | 地址 | 埋点 |
---|---|---|
到家 | /daojia/pages/order/orderList | |
拼团 | /pintuan/pages/order/orderList | |
大电商 | /shopping/pages/order/orderList | |
专柜 | /shopping/pages/offlineOrder/offlineOrderList | |
生活服务 | /deptstore/pages/mall/order/order | |
外卖 | /deptstore/pages/catering/redirect/redirect?type=caterOrder | |
自助埋单(电子小票) | /selfcashiers/pages/ticket/ticketList | |
商品租借 | /rent/index/index?type=rent&page=home/order | |
埋点意义在于:
新的订单列表虽然上线了,保不齐这个庞大的项目中依然有地方跳转到对应业务下的订单列表老页面地址,因此需要进行埋点,观察一段时间之后,如果没有流量进来,就可以对这些老的订单列表页面进行删除;
又或者在这些老页面内部做二次跳转到新的订单列表页面,但是不推荐,因为没有用的代码就应该删掉,不应该保留。
二. 订单列表合并重构方案(新)
目录结构
/order # 订单子模块根目录
├─behaviors **# 操作行为配置模块**
│ cancelBehavior.js # 取消操作
│ commonBehavior.js # 通用化操作
│ daojiaBehavior.js # 天虹到家子模块操作
│ index.js
│ payBehavior.js # 支付操作
│ zujieBehavior.js # 租借子模块操作
├─components **# 组件模块**
│ ├─navigationHeader
│ ├─orderBatchModal
│ ├─panelCard
│ │ └─templates **# 订单卡片模块**
│ │ carParking.wxml # 停车卡片
│ │ pic.wxml # 图片卡片
│ │ picText.wxml # 图文卡片
│ │ proText.wxml # 商品文字卡片
│ │
│ ├─rebuyFailGoodsTips
│ └─selectPanel
├─constants **# 常量模块**
│ handler.js # 操作按钮联调接口 & 参数配置
│ order.js # 订单接口各类参数说明
├─images **# 图片模块**
├─objects **# 接口请求 & UI操作配置模块**
│ order.js
│ orderHandler.js
├─pages **# 页面模块**
│ ├─orderList
│ ├─orderSearch
│ └─refundList
└─utils **# 工具模块**
format.wxs
需求描述
这次需求内容大致上总结:将天虹内部的10几个业态的订单的订单列表进行合并操作。
我列举一下前端能拿到的物料和困难点:
- 一个全业态订单合并后的总接口,可以根据不同业态类型,订单类型入参进行不同业态的切换;
- 各个业务下的不同操作按钮下的接口文档不完善,自行摸索(不同业态的网关路径不一致,入参各不相同);
- 各个操作交互效果不统一,缺乏完善UI素材库支持,自行摸索,小程序部分业务还涉及到H5页面嵌入,十分难搞;
编写思路
我的大致思路就是:将不变的代码固定下来,出于可扩展性考虑,变化的功能代码需要实现可配置化。
UI模板化 —— panelCard
出于避免单组件代码因为出现繁琐的逻辑判断,导致该文件代码编写过长,需要将不同的样式进行template
模板抽离:
// panelCard.wxml
<import src="./templates/picText.wxml"/>
<import src="./templates/pic.wxml"/>
<import src="./templates/carParking.wxml" />
<import src="./templates/proText.wxml" />
<view class="body">
<template wx:if="{{order.cardType === 'picArticle' && order.itemList.length == 1}}" is="picText" data="{{...order.itemList[0]}}" />
<template wx:if="{{order.cardType === 'picArticle' && order.itemList.length > 1}}" is="pic" data="{{order}}" />
<template wx:if="{{order.cardType === 'parking'}}" is="carParking" data="{{order}}" />
<template wx:if="{{order.cardType === 'product'}}" is="proText" data="{{order}}" />
</view>
操作入口统一化 —— panelCard
所有的订单操作按钮均来自这个dom节点:
// panelCard.wxml
<view
class="btnItem {{format.isOperateRed(btn.btnCode) ? 'btnItemRed' : ''}}"
wx:for="{{order.operatorBtnList}}"
wx:key="index"
wx:for-item="btn"
bindtap="operate"
data-btn-code="{{btn.btnCode}}"
data-btn-name="{{btn.btnName}}"
>{{btn.btnName}}</view>
</view>
再来看看组件对应的逻辑代码,只负责将相应的订单数据进行转发,后续所有修改基本不需要修改到这里:
// panelCard.js
/*
* 操作按钮组
*/
operate(e) {
const { btnCode, btnName } = e.currentTarget.dataset
const { orderType } = this.data.order
trackEvent('orderListClick', {
buttonName: btnName,
})
// 判断业态类型
const orderTypeName = ORDER_TYPE[orderType]
// 判断操作方法
const handlerFun = ORDER_OPERATOR[btnCode]
// 执行操作请求
orderHandler[handlerFun].call(this, orderTypeName, this.data.order)
}
操作按钮行为 —— orderHandler
从 panelCard
组件视图层进来之后,根据不同的操作按钮btnCode
值在 /constants/order.js
文件中找到映射具体方法名handlerFun
,进而通过 orderHandler
模块索取对应的操作按钮下的处理函数,将当前的订单类型和订单数据转发过去;
所有的函数名配置说明都可以通过constants/order.js
找到对应的中文说明,这里简单列举一下函数模块:
// orderHandler.js
module.exports = {
appointCheck,
buyAgain,
cancelOrder,
cancelApply,
cancelRefundApply,
confirmMeal,
confirmReceive,
contactShop,
customerService,
delayReceive,
deleteOrder,
deleteRefundOrder,
goExpressDetail,
goOrderDetail,
goRefundOrderDetail,
goServiceComment,
goStorePage,
inviteFriend,
modifyAddress,
pay,
rentBack,
rentGet,
revokeOrder,
showCheckCode,
}
接口 & 操作入参配置化 —— handler.js
这是这次重构的最为关键的一个文件,这里将不同业态下的操作按钮的UI交互行为,不同业态下的接口请求入参和请求接口统一以json文件的格式做了可配置化的处理,先通过一份简单的文档来描述一下配置文件下各参数的用途:
// 操作按钮联调接口 & 参数配置
// 配置解析说明
interface IRequestModel {
type: 'fetch' | 'link' | 'modal'; '发起请求' | '链接跳转' | '二维码弹窗'
host: api.mpHost; 请求网关
path: xxxx; 请求接口地址
url: xxxx; 小程序内部跳转页面地址
callback: IRequestModel; 二次请求接口之后的回调操作
fields: ['fieldA', 'fieldB', ....]; 请求参数,从列表order中获取对应键名的值
fields: [
{
from: 'product', 表示需要商品id,需要从itemList获取
isCustom: true, 表示key是键名,val是对应值
key: 'fieldA', 表示键名
val: 'orderFieldA', 如果isCustomer:false表示从列表order中取值;否则就是默认值
}
....
]
}
举例说明:
-
fetch(发起接口请求,响应操作): 大电商(shopping)和百货(offline)在撤销申请操作表现:
// 撤销申请 revokeOrder: { shopping: { type: 'fetch', host: api.mpHost, path: 'xxxxx', fields: ['orderNo'] // 组成接口入参:{ orderNo: order['orderNo'] } 【ps:order是具体订单数据】 }, offline: { type: 'fetch', host: api.mpHost, path: 'yyyyy', fields: ['orderNo'] } },
-
link(跳转页面):大电商(shopping),百货(offline),group(企业购)在查看物流操作表现:
// 查看物流
goExpressDetail: {
shopping: {
type: 'link',
url: 'xxxxx',
fields: [ // 组成url入参:?order_id=order['orderId'] 【ps:order是具体订单数据】
{
key: 'order_id',
val: 'orderId'
}
]
},
offline: {
type: 'link',
url: 'yyyyyy',
fields: [
{
key: 'orderNo',
val: 'orderNo'
}
]
},
group: {
type: 'link',
url: 'zzzzz',
fields: [
{
key: 'orderNo',
val: 'orderNo'
}
]
}
}
减少通用事件冗余 —— hehaviors
mixin
方案注入会存在有函数名被覆盖的副作用,但是好处就是极大程度减少了代码量的编写(一次编写,多次引入使用),综合这次需求考虑出发,我认为出于后续代码维护和新功能迭代考虑,应该做出取舍:
- 事件导出
// /behaviors/index.js
const cancelBehavior = require('./cancelBehavior');
const commonBehavior = require('./commonBehavior');
const daojiaBehavior = require('./daojiaBehavior');
const payBehavior = require('./payBehavior');
const zujieBehavior = require('./zujieBehavior');
module.exports = {
cancelBehavior,
commonBehavior,
daojiaBehavior,
payBehavior,
zujieBehavior,
}
- 事件引入
// orderList.js(orderSearch —— 【订单搜索】 和refundList —— 【退款列表】同理)
const { cancelBehavior, payBehavior, daojiaBehavior, zujieBehavior, commonBehavior } = require('../../behaviors/index');
Page({
// 本质上就是将所有相关的behavior对象的方法注入到Page对象内部,因为涉及到交互实在是太多了,如果全部放出来,这个文件简直看不下去了,因此采用分治的思想去处理。
behaviors: [
cancelBehavior,
payBehavior,
daojiaBehavior,
zujieBehavior,
commonBehavior
]
})
三. 结语
重构代码是一件十分痛苦的过程,如何在看似若干毫无关联的业务和缺乏完善的物料支持下,做到不遗漏,代码可覆盖率达标,其实测试很重要,这里的测试可能指的不是测试人员的“点点点”这种傻瓜化的测试,而是在完善测试用例的条件下,通过实现类似e2e的端到端的测试用例覆盖,以此来减少测试工作量,提高功能代码的准确率,这是我这次完成了重构之后,需要思考的方向。
在此打个小广告,这是我的github地址,欢迎大家继续关注:
https://github.com/csonchen