背景
最近在开发电商场景的项目时,看到了后端同学给的接口文档上有关于金额的字段都标注了 multiply 100k。询问后得知是为了避免一些精度问题。(前后端数字传输中小数、长整数都会有精度问题存在,不是这篇文章讨论的重点)。
这就导致了下面几个让人头疼的问题:
- 前端在展示接口返回的金额时,需要先除100k
- 有些表单中需要用户填写金额提交时,在请求发出去之前要先乘100k
- 涉及到计算的部分(比如折扣),接口返回值参与计算时要除100k,用户输入值参与计算时不用除
...
满篇的 xxx * 100000
| xxx / 100000
不仅看上去很不优雅,业务逻辑复杂起来以后,代码变得很难维护。
思路
熟练使用axios这个库的前端攻城狮们一定会想到它的 interceptors
。 没错,拦截器终于要发挥它添加请求头之外的作用了😂 (哦不还统一处理过一些异常返回。在拦截器中提前处理了乘除100k的操作以后,业务代码中拿着数字直接用,一个字,爽。
在开始写代码前要考虑先构思一下:
- 首先要知道哪些字段需要去做乘除的
- 对于目标字段不同数据类型的处理逻辑
代码
话不多说,我们逐步看看怎么实现
AxiosRequestConfig
中新增路径传参
上文提到过需要知道哪些字段需要去做处理,这边的解决方案是在 AxiosRequestConfig
中添加一个 parse100kFields
字段,接收的是一个字符串数组。
而每个字符串则代表目标字段的路径
const data = {
single: [
{
price: 100000,
},
{
price: 200000,
}
],
total: 1234000,
}
// 例如这里需要修改 price 和 total 两个字段的值
const config = {
//...
parse100kFields: [
'single.price',
'total'
]
}
为了TypeScript项目中接口声明时添加此config不报错,可以添加 d.ts 文件更改 AxiosRequestConfig
类型声明
// config.d.ts
import "axios";
declare module "axios" {
export interface AxiosRequestConfig {
parse100kFields?: string[];
}
}
request/response中处理对应路径下的字段
这一步要注意的是处理对象或数组的每一层都要保持引用是相同的,这里的原因是找到要修改的字段直接修改它比较容易,但如果要恢复原对象的整个数据结构会变得很麻烦。
const parse100k = (obj: any, path: string, type: 'multiply' | 'divide') => {
path = path.replace(/^\./, '');
const keyArr = path.split('.');
// 路径每解析一层就把下一层需要解析的元素都放进来 (主要是要处理字段类型为数组的子元素)
let container = [obj];
try {
for (let i = 0; i < keyArr.length; i++) {
const key = keyArr[i];
const newContainer = [];
container.forEach(singleObj => {
if (key in singleObj) {
if (i === keyArr.length - 1) {
// 若无下一层路径,则做转换
singleObj[key] = numberParser(singleObj[key], type);
} else {
const nextLevelElement = singleObj[key];
if (Object.prototype.toString.call(nextLevelElement) === "[object Object]") {
newContainer.push(nextLevelElement)
} else if (Object.prototype.toString.call(nextLevelElement) === "[object Array]") {
newContainer.push(...nextLevelElement)
} else {
// 字段类型非数组或对象,则无法往下层解析
throw new Error()
}
}
container = newContainer;
} else {
throw new Error()
}
})
}
return obj;
} catch (err) {
console.warn(`Cannot find ${path} in obj`);
return obj;
}
}
数字的处理
这里仅处理 number
类型和可以转成 number 的 string
类型,除此之外把原本的数据返回回去。
这里用到了一个轻量的解决JS计算精度的库 number-precision
import NP from 'number-precision';
const numberParser = (number: any, type: 'multiply' | 'divide') => {
// 如果类型不能做转换,保持不变
if (!['number', 'string'].includes(typeof number)) {
return number;
}
const CONVERTER = type === 'multiply' ? 100000 : 0.00001;
if (typeof number === 'number') {
return NP.times(number, CONVERTER);
}
// 字符串类型没法被转换成数字的话,保持不变
return isNaN(Number(number)) ? number : String(NP.times(Number(number), CONVERTER));
}
FormData的处理
在一些包含文件传输的接口里会用到 FormData
的类型传输,这种类型的处理方法与普通对象有所不同
const parse100kFormData = (formData: FormData, path: string) => {
const parsedFields = formData.getAll(path).map(value => numberParser(value, 'multiply'));
formData.delete(path);
parsedFields.forEach(value => {
formData.append(path, value);
})
return formData;
}
interceptors
最终就是 request / response 的interceptor啦
const responseNumberHandler = (response: AxiosResponse) => {
const { data } = response.data;
const { parse100kFields = [] as string[] } = response.config;
if (data) {
parse100kFields.forEach(filed => {
parse100k(data, filed, 'divide');
})
}
return response.data;
}
const requestNumberHandler = (config: AxiosRequestConfig) => {
const { parse100kFields = [] as string[], method, data } = config;
if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) && data) {
parse100kFields.forEach(field => {
(data instanceof FormData) ? parse100kFormData(data, field) : parse100k(data, field, 'multiply');
})
}
return config;
}
net.interceptors.request.use(requestInterceptor);
net.interceptors.response.use(responseNumberHandler);
最终效果
对下边的接口进行配置
export const getBillingDetail = (params: GetBillingDetailReq) => {
return net.get(`${PREFIX}/billing/detail`, { params,
parse100kFields: ['order_amount_after_tax']
});
};
在控制台打印一下转换完毕后的数据结构
这样在业务代码里完全不用考虑乘除100k的问题了!乃思!
如果有更好的实现方式和没考虑到的边界情况,欢迎大佬们指教。