阅读 194

后端接口要求金额都乘100k?一个axios拦截器搞定

背景

最近在开发电商场景的项目时,看到了后端同学给的接口文档上有关于金额的字段都标注了 multiply 100k。询问后得知是为了避免一些精度问题。(前后端数字传输中小数、长整数都会有精度问题存在,不是这篇文章讨论的重点)。

这就导致了下面几个让人头疼的问题:

  1. 前端在展示接口返回的金额时,需要先除100k

  2. 有些表单中需要用户填写金额提交时,在请求发出去之前要先乘100k

  3. 涉及到计算的部分(比如折扣),接口返回值参与计算时要除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']   }); }; 复制代码

在控制台打印一下转换完毕后的数据结构

Jan-17-2022 21-38-33.gif

这样在业务代码里完全不用考虑乘除100k的问题了!乃思!

如果有更好的实现方式和没考虑到的边界情况,欢迎大佬们指教。


作者:天光墟叻仔筠
链接:https://juejin.cn/post/7054172931970039844


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐