background

When I was developing the project of emporium recently, I saw the amount fields labeled multiply 100K in the interface document given by my classmates at the back end. It was asked to avoid some accuracy problems. (There are accuracy problems in both small and long integers at the front and back end of digital transmission, which is not the focus of this article).

This leads to the following headaches:

  1. When displaying the amount returned by the interface, the front-end needs to divide 100K first
  2. Some forms require the user to submit an amount, multiplied by 100K before the request is sent
  3. For the part involved in calculation (such as discount), 100K shall be divided when the returned value of the interface participates in calculation, but not when the input value of the user participates in calculation

.

* 100000 | full of XXX XXX / 100000 looks very elegant, not only the business logic is complicated, the code becomes difficult to maintain.

Train of thought

Those familiar with the front-end of the Axios library will be reminded of its interceptors. That’s right, the interceptor is finally going to do more than add headers 😂 (oh no, and handle some exception returns as well. After handling the 100K multiplication and division operation in advance in the interceptor, the business code directly uses the number, a word, cool.

Before you start writing code, consider:

  • The first step is to know which fields need to be multiplied and divided
  • Processing logic for different data types of the target field

code

So without further ado, how do we do that

AxiosRequestConfigAdded path transmission parameter in

As mentioned above, you need to know which fields need to be processed. The solution here is to add a parse100kFields field in AxiosRequestConfig that receives an array of strings.

Each string represents the path to the target field

const data = {
  single: [{price: 100000}, {price: 200000,}],total: 1234000,}// For example, the price and total fields need to be changed
const config = {
  / /...
  parse100kFields: [
    'single.price'.'total']}Copy the code

To add this config to a TypeScript project without an error when interface declarations are added, you can change the AxiosRequestConfig type declaration by adding a D.ts file

// config.d.ts
import "axios";

declare module "axios" {
  export interfaceAxiosRequestConfig { parse100kFields? :string[]; }}Copy the code

Fields in the corresponding path are processed in request/ Response

The important thing to note in this step is to keep references the same for each layer of the object or array. The reason is that it is easier to find the field to change and modify it directly, but it is more cumbersome to restore the entire data structure of the original object.

const parse100k = (obj: any, path: string.type: 'multiply' | 'divide') = > {
  path = path.replace(/ ^ \. /.' ');
  const keyArr = path.split('. ');

  // Each layer of the path parses elements that need to be parsed in the next layer.
  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) {
            // If there is no path at the next level, perform the conversion
            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 {
              // If the field type is not array or object, it cannot be parsed down
              throw new Error()
            }
          }
          container = newContainer;
        } else {
          throw new Error()}})}return obj;
  } catch (err) {
    console.warn(`Cannot find ${path} in obj`);
    returnobj; }}Copy the code

Number processing

We only deal with numbers and strings that can be converted to numbers, and otherwise return the original data.

Here used a lightweight solution to the JS calculation precision of the library number-precision

import NP from 'number-precision';

const numberParser = (number: any.type: 'multiply' | 'divide') = > {
  // If the type cannot be converted, leave it unchanged
  if(! ['number'.'string'].includes(typeof number)) {
    return number;
  }

  const CONVERTER = type= = ='multiply' ? 100000 : 0.00001;

  if (typeof number= = ='number') {
    return NP.times(number, CONVERTER);
  }

  // If the string type cannot be converted to a number, it remains the same
  return isNaN(Number(number))?number : String(NP.times(Number(number), CONVERTER));
}
Copy the code

The processing of FormData

The FormData type transfer is used in some interfaces that contain file transfers, which are handled differently from normal objects

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;
}
Copy the code

interceptors

Finally, it’s the 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);
Copy the code

The final result

Configure the following interfaces

export const getBillingDetail = (params: GetBillingDetailReq) = > {
  return net.get(`${PREFIX}/billing/detail`, { params,
    parse100kFields: ['order_amount_after_tax']}); };Copy the code

Print the converted data structure on the console

In this way, the business code does not have to consider the multiplication and division of 100K problem! Is thinking!

If there is a better way to implement it and the boundary case that is not taken into account, we welcome your advice.