闲鱼作为国内最大的二手交易电商平台,有着验货宝/省心卖/优品等有着闲鱼特色的交易链路,而作为交易链路的闭环,一旦形成有效订单,就会有对应的订单详情页,订单详情页往往承载着复杂的交易状态的变化。以验货宝为例, 验货宝是闲鱼推出针对二手商品存在的质量/真伪的不确定性,提出的先验货后交易模式。
作为交易订单节点:买家拍下->付款->卖家发货->鉴定方收货->鉴定为真->发货给买家->买家收货等多个交易节点,一个订单详情页在不同流转状态下可能存在数十种细分状态,每种状态之间存在着有着文案/操作按钮/进度条等视觉交互上的差别, 如下图所示:
订单详情页的不同的状态视图流转,依赖于接口返回的字段的改变,存在和服务端的沟通成本;
前端的直接mock的方式,像集团内的山海关,dummy更多是通用的mock数据映射,本质还是要手动修改mock数据的status值;
在真机调试时,依赖的Charles的pc代理,单次修改成本高;
除了开发同学外,测试同学测试回归的成本高. UI同学在视觉走查中难以覆盖全部,容易带问题到线上,引起线上舆情。
总结来说,面对此类订单的订单详情页的前端开发测试,我们现有的mock存在着以下问题:
•开发联调周期长,自测难以充分; •测试/视觉回归成本高; •线上样式问题定位还原周期长;
针对以上的问题,能否让订单状态页面的mock方式更为易用,我们通过真实的开发体验,提出了针对此类页面的mock方式要达到好用,应该具备以下特点:
1.调试方便
mock方式应该在本地pc端调试和真机调试都能够方便使用;
2.业务语义
不同状态的切换交互应该带有业务语义,能够方便开发和测试快速找到想要的订单状态,而不只是简单的修改接口的某个字段值;
3.代码解耦
mock的方式本身应该和业务代码尽量解耦,即不会将mock的逻辑引入线上环境;
4.mock数据精简
订单页面状态虽多,但是接口往往统一,接口字段,对mock数据的维护不应该每一种状态都是单独一份mock数据而是应该在一份主的mock数据上针对产生变化的字段进行单独mock;
基于以上需求,我们尝试设计开发更为贴近此类业务的mock方案。先别看广告,看疗效, 此方案的真实效果如下图所示:
从效果图可以看出我们通过按钮点击实现了状态视图的变更,也就实现了目标一、二,而这个按钮本身不会在线上环境所带入也就不实现了代码解耦;那么这套方案是如何实现的呢?如下图所示,我们将整体方案设计为三大模块:
胶水层:是整个方案的编译层基座,负责在编译层将视图层插件和mock层插件按照生产环境状态加载进入业务层,实现在本地和预发环境下具有mock能力,并负责在视图层进行状态切换时按照状态码重新生成对应mock.json,利用胶水层的打包逻辑做到了与业务解耦的目标;
mock层:负责将前端发起的mtop请求拦截,根据路由映射到本地webserver下的mock.json并模拟返回对应结果,利用mock层方式实现了mock数据精简,调试方便的目标;
视图层:负责页面处的整体mock状态切换的交互逻辑,我们将交互入口设计为类似于eruda调试工具的唤起方式,在需要的订单页面侧植入, 点击会唤起弹层,弹层展示所有可枚举的订单状态文案以及其他可枚举会影响订单视图的变量条件,点击对应状态进行页面重载展示对应状态的视觉; 视图层负责实现了业务语义的目标。
在具体实现过程中,由于闲鱼前端开发使用集团提供的rax的前端方案,rax-app也提供了在编译层可以定制的插件机制, rax-app基于工程构建工具 build-scripts 封装,因此在插件能力上也完整继承了build-scripts。除了通过插件定制工程能力以外,rax-app 还为插件扩展了运行时定制的能力, 我们定制了@ali/build-plugin-rax-mock和本地的selfBuild两个编译插件,分别对应mock层和胶水层的设计需求,以如下方式在项目的build.json里进行引入,很好地实现与业务层代码的解耦。
{
"plugins": [
[
"build-plugin-rax-app",
{
"targets": [
"web"
],
"type": "mpa"
}
],
"@ali/build-plugin-rax-mock",
"./selfBuild"
]
}
rax-app的插件机制提供的针对webpack打包方案所提供的onGetWebpackConfig api将视图层组件植入业务页面, 如下方代码所示,会在打包过程中根据根据指定页面文件路径选择性地注入,并且判断编译环境在真实生产环境中不做任何mock模块的打包。
if (api.context.command === 'build') return;
api.onGetWebpackConfig('web', (config) => {
config.entryPoints.values().forEach(entry => {
const entrys = entry.values();
const entryName = entrys[1];
// 只对订单页面注入
if (!/pages\/Order\/index$/.test(entryName)) {
return;
}
const prefixLoader = __filename;
const debugOrderPath = path.resolve(__dirname, 'src/components/DebugOrder');
const newOrderPage = `${prefixLoader}?debugOrderPath=${debugOrderPath}!${entryName}`;
entry.clear();
// 视图层组件
entry.add(entrys[0]);
entry.add(newOrderPage);
});
});
}
因此通过胶水层,我们可以快速将mock模块和视图层模块快速引入到工程方案中,并能按需引入,不对业务造成明显侵入痕迹。
首先我们会根据状态合集所需订单详情接口生成一份mock.json数据的合集,这样就能涵盖所有状态下所需消费字段,一般接口格式如下所示:
"api": "mtop.a.order.info",
"data": {
"status": 0,
"orderStatus": 1001,
...
"trade": {
"actions": [],
....
"amount": "2189.00",
"attributes": {
"consis": "10",
...
"ultronPP": "a_3_0@c",
}
}
},
"ret": [
"SUCCESS::调用成功"
],
"v": "1.0"
}
如下图所示,mock层既要更新视图层切换状态而组成新的mock.json, 也要拦截页面侧发起的mtop网络请求定向到对应的mock.json文件;
为了真实能够把接口请求到本地,需要对h5页面侧发起的mtop请求进行拦截,这里利用了淘宝mtop库的运行机制会将mtop对象加载到页面全局的window.lib对象上,利用Proxy的代理机制监听window.lib对象的挂载mtop时机set进行hook, 并根据所处的生产环境判断是否使用发送请求到本地的自定义request请求, 实现代码如下:
lib = window.lib;
// 拦截Mtop对象的request方法挂载
const getMtop = (originValue) =>
new Proxy(originValue, {
set(target, p, v, r) {
if (p === 'request'
|| p === 'H5Request') {
Reflect.set(target, p, getRequest(v), r);
} else {
Reflect.set(target, p, v, r);
}
return true;
}
});
// 拦截window.lib对象挂载mtop
if (!lib) {
lib = new Proxy({}, {
set(target, p, v, r) {
if (p.toLowerCase() === 'mtop') {
Reflect.set(target, p, v,r);
} else {
Reflect.set(target, p, v, r);
}
return true;
}
});
} else if (!lib.mtop) {
lib.mtop = getMtop({});
} else {
lib.mtop.request = getRequest(lib.mtop.request);
lib.mtop.H5Request = getRequest(lib.mtop.H5Request);
}
// 根据运行环境选择加载对应的request请求
function getRequest(originRequest) {
return async function () {
if (getMockSwitch()) {
//
}
return originRequest();
}
}
当挂载完成即可实现接口的mock功能:
- 当本地页面发起 mtop 请求,如:mtop.com.test.one;
- 请求被注入的插件代码 hold 住,当判断是在本地开发环境或者链接带上mock query时, 用本地的mtopRquest替换,请求以 http://127.0.0.1/_mtop_mock_/com.test.one 格式重新发起请求;
- 请求打到本地 webpack-dev-server 上,server 再去本地 mock 目录上找 com.test.one.js4.如果找到就执行 com.test.one.js 文件,将执行结果返回5.如果未找到,则走原有的 mtop 请求;
视图层UI由页面侧直接可见的切换icon和弹层构成,icon设置为dragable方便用户随时拖动, 弹层的渲染考虑到扩展性由树状节点组成。目前根据真实的业务需要,将节点层级分为2层(可扩展),一级节点代表:已下单,已验货等核心节点, 二级节点代表在一级节点下可能存在的正负向交易细节节点, 如:在已下单环节下存在取消订单的副状态;单点数据结构为如下代码所示,包含有每个枚举状态的语义文案,每种状态对应接口字段的主副状态码,状态枚举值,子节点。
// 状态数据结构
export interface ClassifyDataItem {
[key: string]: any;
/**
* 节点名称
*/
nodeName: string;
/**
* 节点枚举值
*/
node?: StatusEnum;
/**
* 对应的协议主状态
*/
status?: string;
/**
* 对应的协议副状态
*/
subStatus?: string;
childNode?: ClassifyDataItem[];
}
// 真实的状态数据枚举
export const classifyData: ClassifyDataItem[] = [
{
nodeName: '已下单,等待顺丰上门取件',
status: '1',
subStatus: '10',
node: StatusEnum.BUYER_CREATE,
childNode: [
...classifyData
]
}
...
];
每个节点在渲染过程中采用树状递归渲染,视觉层展示nodeName, 同时绑定点击回调onItemChange。当弹窗的某一状态结点发生点击行为时,设置节点选中态样式,同时通过Modal组件通过props传入的函数方法向外层传递绑定在结点上的status和subStatus值,从而实现了点击单个选项能够修改对应的mock.json文件, 同时会触发页面的window.reload()功能重新加载页面发起mtop请求,因此此时接口返回的数据已经是切换后的状态数据,因此页面也会呈现对应状态的视图。
当将以上所述的整套方案串联完成后,业务侧开发只需要定义好属于自己订单详情的状态枚举数据结构,并将视图层组件和mock层插件引入项目工程,即可接入mock的能力,实现所选即所得的订单状态变更。
目前该方案已经在闲鱼验货宝、奢侈品寄卖等交易链路场景下接入, 从真实使用过程中,能够有效提升开发联调的效率,单次状态的切换时间成本从分钟级别下降到秒级, 并能帮助测试和视觉同学快速回归业务场景,定位线上问题,简单的视觉问题不再依赖于后端接口的状态切换,约能节省30%以上的联调沟通成本。另外带来的彩蛋是该方案的切换语义特点, 在项目开发人员出现流动时,新承接人能够通过这一mock方式快速地理解业务,上手开发。上手快,所见即所得,算是针对此类页面维护开发的一门利器。
本章节主要介绍了闲鱼在前端开发典型交易场景-订单详情页时,面对多状态模式切换页面的一种开发体验提升的一种尝试,通过上述的方案设计基本实现了开篇所定义的业务mock的4个目标:调试方便、业务语义、代码解耦、mock数据精简,不仅提升了在开发/测试同学的体验,通过语义化的方式也能帮助项目更好地进行维护升级。
伴随着业务系统的复杂度提升和功能迭代,如何能如何更低成本地去维护mock项目给我们提出了更高的要求,能否在比如状态码伴随业务升级,变更,增减的功能迭代过程中,联动式地快速变更枚举状态值配置,减少业务侧开发的维护成本是我们继续升级迭代该方案的方向。目前页面只针对单页面mock, 如何去覆盖整体交易链路的mock,比如一个完整订单链路的状态一致性,也是值得探索的方向。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8