30

background

Concurrency conflict problem is a relatively common problem in daily development.

Different users change data in a short time interval, or repeated submission operations performed by a certain user may cause concurrency conflicts.

Concurrency scenarios are difficult to investigate comprehensively in the development and testing stages, and it is difficult to locate online bugs after online bugs. Therefore, concurrency control is a problem that needs to be paid attention to in the front-end and back-end development process.

For the problem of repeated submission of data by the same user in a short period of time, the front-end can usually do a layer of interception first.

This article will discuss how the front-end uses axios' interceptor to filter repeated requests and resolve concurrency conflicts.

General processing method-add loading every time a request is sent

Before trying the axios interceptor, let’s take a look at how our previous business dealt with concurrency conflicts:

Every time the user manipulates the controls (input boxes, buttons, etc.) on the page and sends a request to the backend, a loading effect is added to the corresponding control on the page, prompting that the data is being loaded, and it also prevents the user from continuing the operation before the loading effect ends. Control.

This is the most direct and effective way. If your front-end team members are careful and patient enough and have good coding habits, this can solve the concurrency problems caused by most users accidentally repeating submissions.

A better solution: axios interceptor unified processing

There are so many scenarios in the project that require the front-end to limit concurrency. Of course, we have to think about better and more trouble-free solutions.

Since the concurrency control is performed every time a request is sent, if the public functions of the issued request can be repackaged, and the repeated requests can be processed uniformly to achieve automatic interception, our business code can be greatly simplified.

axios library used by the project to send http requests, axios officially provides us with a wealth of APIs, let’s take a look at the two core APIs needed to intercept requests:

1. interceptors

Interceptors include request interceptors and response interceptors, which can be intercepted before the request is sent or after the response. The usage is as follows:

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  return config;
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });

2. cancel token:

Call cancel token API to cancel the request. The official website provides two ways to build cancel token . We use this method: create cancel token executor function to the CancelToken constructor, so that repeated requests can be executed immediately when repeated requests are detected in the above request interceptor:

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    cancel = c;
  })
});

// cancel the request
cancel();

The idea provided in this article is to use axios interceptors API intercept the request, check whether there are multiple identical requests in the pending state at the same time, and if so, call cancel token API cancel the repeated request.

If the user repeatedly clicks the button and submits two identical requests, A and B (considering the request path, method, and parameters), we can choose one of the following interception schemes:

  • Cancel request A, only send request B
  • Cancel B request, only issue A request
  • Cancel request B, only send request A, and use the returned result of request A as the return result of request B

The third solution requires monitoring processing to increase the complexity, combined with our actual business needs, and finally adopted the second solution to achieve, namely:

only sends the first request. When the A request is still in the pending state, all subsequent requests that are repeated with A are cancelled, and only the A request is actually issued. The interception of this request is stopped until the A request ends (success/failure).

Implementation

  1. Store all pending requests

First, we need to store all pending status requests in the project in a variable, call it pendingRequests ,

You can axios as a singleton class or define global variables to ensure that the pendingRequests variable can be accessed before each request is sent, and check whether it is a repeated request.

let pendingRequests = new Map()

Combine the method, url, and parameters of each request into a string, which serves as the unique key to identify the request and also the key of the pendingRequests

const requestKey = `${config.url}/${JSON.stringify(config.params)}/${JSON.stringify(config.data)}&request_type=${config.method}`;

Little tips to help understand:

  • The purpose of defining pendingRequests as a map object is to facilitate us to query whether it contains a certain key, and to add and delete keys. When adding a key, the corresponding value can set some user-defined function parameters, which will be used when expanding the function later.
  • config is axios interceptor, which contains the information of the current request
  1. Check whether the current request is repeated before the request is sent

    In the request interceptor, generate the above requestKey and check pendingRequests object contains the current request requestKey

    • Yes: It means that it is a repeated request, cancel the current request
    • No: add requestKey to the pendingRequests object

Because behind the response have to use interceptors in the current request requestKey , in order to avoid stepping pit, it is best not to run again, at this stage put requestKey stored back axios interceptor config parameters, it can be followed directly in response interceptor response.config.requestKey it through 0609636f94e3a5.

Code example:

// 请求拦截器
axios.interceptors.request.use(
  (config) => {
    if (pendingRequests.has(requestKey)) {
      config.cancelToken = new axios.CancelToken((cancel) => {
        // cancel 函数的参数会作为 promise 的 error 被捕获
        cancel(`重复的请求被主动拦截: ${requestKey}`);
      });
    } else {
      pendingRequests.set(requestKey, config);
      config.requestKey = requestKey;
    }
    return config;
  },
  (error) => {
    // 这里出现错误可能是网络波动造成的,清空 pendingRequests 对象
    pendingRequests.clear();
    return Promise.reject(error);
  }
);
  1. pendingRequests object after the request is returned

If the request has successfully reached the step of the response interceptor, it means that the request has ended in the pending state, then we have to remove it from pendingRequests :

axios.interceptors.response.use((response) => {
  const requestKey = response.config.requestKey;
  pendingRequests.delete(requestKey);
  return Promise.resolve(response);
}, (error) => {
  if (axios.isCancel(error)) {
    console.warn(error);
    return Promise.reject(error);
  }
  pendingRequests.clear();
  return Promise.reject(error);
})
  1. Need to clear the scene of the pendingRequests

When a request error occurs due to network fluctuations or timeouts, you need to clear all previously stored request records in the pending state. The code demonstrated above has been commented.

In addition, the page also needs to be cleared before switching cache pendingRequests object, you can use Vue Router of beforeEach hook:

router.beforeEach((to, from, next) => {
  request.clearRequestList();
  next();
});

Function extension

  1. Unified processing interface error prompt

Agree on the format of the data returned by the interface with the backend, and in the case of an interface error, you can add toast to the user in a unified response interceptor.

For special interfaces that do not need to report an error, you can set a parameter and store axios in the config parameter of the 0609636f94e666 interceptor to filter out the error message:

// 接口返回 retcode 不为 0 时需要报错,请求设置了 noError 为 true 则这个接口不报错 
if (
  response.data.retcode &&
  !response.config.noError
) {
  if (response.data.message) {
    Vue.prototype.$message({
      showClose: true,
      message: response.data.message,
      type: 'error',
    });
  }
  return Promise.reject(response.data);
}
  1. Add loading effect to the control when sending the request

When using axios interceptors filter repeated requests above, you can throw information on the console to remind the developer. On this basis, if you can add loading effects to the controls operated on the page, it will be more user-friendly.

Common ui component libraries provide loading services, and you can specify the controls that need to add loading effects on the page. The following is an example code element UI

// 给 loadingTarget 对应的控件添加 loading 效果,储存 loadingService 实例
addLoading(config) {
  if (!document.querySelector(config.loadingTarget)) return;
  config.loadingService = Loading.service({
    target: config.loadingTarget,
  });
}

// 调用 loadingService 实例的 close 方法关闭对应元素的 loading 效果
closeLoading(config) {
  config.loadingService && config.loadingService.close();
}

Similar to the above filtering error method, the class name or id of the element is stored in the config parameter of the axios

Call request interceptor addLoading method, in response to the call interceptor closeLoading method specified control can be achieved (e.g., Button) loading request pending during the control is automatically canceled after the loading effect of the request.

  1. Support the combined use of multiple interceptors

Simply look at the axios interceptors part of the implementation source code to understand, it supports the definition of multiple interceptors , so as long as the interceptors defined by us conforms to the Promise.then chain call specification, more functions can be added:

this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
  chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
  chain.push(interceptor.fulfilled, interceptor.rejected);
});

while (chain.length) {
  promise = promise.then(chain.shift(), chain.shift());
}

to sum up

Concurrency problems are very common and relatively cumbersome to deal with. When the front-end resolves concurrency conflicts, the axios interceptor can be used to uniformly process repeated requests and simplify business code.

At the same time, the axios interceptor supports more applications. This article provides the implementation of some commonly used extended functions. Interested students can continue to explore other uses of supplementary interceptors.

Today's content is so much, I hope it will help you.

If you find the content helpful, you can follow my official account, keep up with the latest developments, and learn together!

image.png


皮小蛋
8k 声望12.8k 粉丝

积跬步,至千里。