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

3,340 阅读3分钟

背景

最近在开发电商场景的项目时,看到了后端同学给的接口文档上有关于金额的字段都标注了 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的问题了!乃思!

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