link

Your application needs a video version of the Cancelable asynchronous HTTP request module

github

How do I cancel an asynchronous HTTP request?

Asynchronous HTTP requests are ubiquitous in modern Web applications. In order to better user experience, Ajax appeared in 2005, support to achieve local updates without refreshing the page.

Ajax supports both synchronous and asynchronous methods, but most people only use asynchronous methods, because sending synchronous requests will cause the browser to enter a temporary state of suspended animation, especially when the request needs to deal with a large amount of data, long waiting interface, in this case, using synchronous requests, will bring very bad user experience. So asynchronous requests are common, and that’s where today’s topic comes in. You might need an asynchronous HTTP request that can be canceled. Here are some questions to consider:

  • Why do YOU need cancelable asynchronous HTTP requests?

    It’s not uncommon to send an HTTP request and suddenly not need the result while waiting for an interface response.

  • In what circumstances would it be used?

    For example, if you have multiple tabs on a page, click on each Tab to send a corresponding HTTP request, and then display the result of the request in the content area. Now, when the user clicks the Tab1 TAB, the result of the interface 1 request is obtained. However, after clicking Tab2, the interface needs to wait for 3s to return. Therefore, the user clicks Tab3 directly, and the interface Tab2 return result is no longer needed.

  • What does it do?

    If you do not cancel the Tab2 request, the content area will look like this: If you do not cancel the asynchronous HTTP request sent by Tab2, the test will ask you to find the bug.

The following two animations illustrate the above process to help you understand the scene more clearly. The first animation is normal, clicking Tab 2 and waiting for the interface to return before continuing to click Tab 3. However, the second animation demonstrates this abnormal phenomenon. After clicking Tab 2, click Tab 3 directly without waiting for the interface to return, and find the contents of Tab 3 interface first display, and then change to Tab 2 interface result.


Such requirements and scenarios are common in modern Web application development. People just at ordinary times less likely to realize that there will be problems, because the bug that exists only in need after a long wait to get the response results of interface (Tab. 2, for example), so if you need to processing and transmission interface to the application of large amounts of data or application when used in weak network environment, are likely to encounter this problem. Therefore, a mature, stable Web application must support cancelable asynchronous HTTP requests.

The sample

In modern Web application development, a common approach is to encapsulate a common HTTP request module, which is usually based on third-party open source libraries (such as Axios) or native methods (such as the Fetch API, XMLHttpRequest).

Next, we will use an example to simulate a real project scene. In fact, the above animation comes from a real project development, and it is not convenient to provide actual cases, so we use examples to simulate. The HTTP request module in the case is implemented through Axios, Fetch API and XMLHttpRequest respectively.

The service side

The express framework is used to realize the server and node Server. js or Nodemon Server.js is used to start the server

const app = require('express') ()const cors = require('cors')
app.use(cors())

app.get('/tab1'.(req, res) = > {
  res.json('Tab 1 results')
})

app.get('/tab2'.(req, res) = > {
  // Here we use delay code to simulate the scenario of processing large amount of data
  setTimeout(() = > {
    res.json('Tab 2 results')},3000)
})

app.get('/tab3'.(req, res) = > {
  res.json('Tab 3 results')
})

app.listen(3000.() = > {
  console.info('app start at 3000 port')})Copy the code

The front end

index.html

<! DOCTYPEhtml>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Document</title>
  <style>
    #content {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 200px;
      height: 100px;
      border: 1px solid #eee;
    }
  </style>
</head>

<body>
  <! Content display area -->
  <h3>Content area</h3>
  <p id="content">no content</p>

  <! -- Three buttons to simulate three tabs -->
  <button id="tab1">Tab 1</button>
  <button id="tab2">Tab 2</button>
  <button id="tab3">Tab 3</button>

  <button id="reset">reset</button>

  <! -- axios -->
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  <! Request interface based on AXIos encapsulation -->
  <script src="./axiosRequest.js"></script>
  <! -- Request interface based on FETCH encapsulation -->
  <! -- <script src="./fetchRequest.js"></script> -->
  <! Request interface based on XMLHttpRequest encapsulation -->
  <! -- <script src="./xhrRequest.js"></script> -->

  <script>
    // Add click events to the three buttons, execute the corresponding callback function to request the corresponding interface when clicked, and display the result to the content area after successful request
    const tab1 = document.querySelector('#tab1')
    const tab2 = document.querySelector('#tab2')
    const tab3 = document.querySelector('#tab3')

    // Content display area
    const content = document.querySelector('#content')

    // reset
    const reset = document.querySelector('#reset')

    tab1.addEventListener('click'.async function () {
      const { data } = await request({ url: '/tab1' })
      content.textContent = data
    })

    tab2.addEventListener('click'.async function () {
      const { data } = await request({ url: '/tab2' }, true)
      content.textContent = data
    })

    tab3.addEventListener('click'.async function () {
      const { data } = await request({ url: '/tab3' })
      content.textContent = data
    })

    reset.addEventListener('click'.function () {
      content.textContent = 'no content'
    })

  </script>
</body>

</html>

Copy the code

axiosRequest.js

The Request interface, wrapped in Axios, can be extended to suit the needs of the business, typically in both interception areas

const baseURL = 'http://localhost:3000'

const ins = axios.create({
  baseURL,
  timeout: 10000
})

ins.interceptors.request.use(config= > {
  // Intercepting requests, where you can customize some configurations, such as tokens
  return config
})

ins.interceptors.response.use(response= > {
  // Intercept the response, according to the status code returned by the server to do some custom response and information prompt
  return response
})

function request(reqArgs) {
  return ins.request(reqArgs)
}

Copy the code

fetchRequest.js

Request interface based on FETCH API encapsulation, encapsulation is simple, just to illustrate the problem; The data format returned is compatible with the sample code based on axiosRequest

const baseURL = 'http://localhost:3000'

function request(reqArgs) {
  // The data format returned by the interface is designed to be compatible with axiOS sample code
  return fetch(baseURL + reqArgs.url).then(async response => ({ data: await response.json() }))
}

Copy the code

xhrRequest.js

Request interface based on XMLHttpRequest API encapsulation, encapsulation is simple, only to illustrate the problem; The data format returned is compatible with the sample code based on axiosRequest

const baseURL = 'http://localhost:3000'

const xhr = new XMLHttpRequest()
function request(reqArgs) {
  return new Promise((resolve, reject) = > {
    xhr.onload = function () {
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
        // The data format returned by the interface is designed to be compatible with axiOS sample code
        resolve({ data: xhr.responseText })
      } else {
        / / make a mistake
        reject(xhr.status)
      }
    }
    xhr.open(reqArgs.method || 'get', baseURL + reqArgs.url, true)
    xhr.send(reqArgs.data || null)})}Copy the code

The solution

Here is how to adapt an existing application to achieve a secure and seamless upgrade with minimal changes. For a new project, simply integrate the following solutions into the architecture.

Solutions fall into two categories:

  • A native method

    Axios, the Fetch API, and the XMLHttpRequest native all provide the ability to cancel asynchronous HTTP requests, though some are less useful, such as the Fetch API.

  • Common methods

    I prefer the generic approach, which is easy to understand and doesn’t require memorizing various native methods.

Cancelable promises

Before entering the formal transformation, let’s first popularize a knowledge, how to cancel a Promise?

As you all know, once the logic of a Promise is started, it cannot be stopped until it is completed. So we often encounter situations where asynchronous logic is normally processed but the program no longer needs the result, much like our case. It would be nice to cancel promises at this point, and some third-party libraries, such as Axios, provide this feature. In fact, the TC39 committee was ready to add this feature, but the proposal was withdrawn. As a result, ES6’s Promise was considered “radical.”

In fact, we can use the Promise feature to provide a kind of temporary encapsulation to achieve functionality (but knowledge) similar to the cancellation of promises. We all know that once the state of Promise changes from pending to fulfilled or rejected, it cannot be changed again.

const p = new Promise((resolve, reject) = > {
  resolve('result message')
  // Resolve will be ignored
  resolve('I was ignored... ')
  console.log('I am running !! ')})// I am running!!

// Promise {<fulfilled>: "result message"}
console.log(p)

Copy the code

We can use this feature to implement a cancelable Promise. You can expose a cancel function that is called when a Promise needs to be canceled, which executes the Promise’s resovle or Reject methods when called, so that resolve or Reject when the interface gets a response is ignored. This is a way to implement something like cancel promises.

<! -- Cancelable Promise -->

<! DOCTYPEhtml>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Document</title>
  <style>
    #result {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 200px;
      height: 100px;
      border: 1px solid #eee;
    }
  </style>
</head>

<body>
  <! -- Display request result -->
  <h3>Request the results</h3>
  <p id="result">no result</p>

  <! -- Three buttons: request button, cancel button, reset button -->
  <button id="req1">req 1</button>
  <button id="cancel">cancel</button>
  <button id="reset">reset</button>

  <script>
    // Expose the interface that cancels promises
    let cancelReq = null
    // Expose the Request interface
    function request(reqArgs) {
      return new Promise((resolve, reject) = > {
        // Simulate asynchronous HTTP requests with delay code
        setTimeout(() = > {
          resolve('result message')},2000);

        // Give the user a function to cancel the request
        cancelReq = function () {
          resolve('Request has been cancelled')
          cancelReq = null}})}</script>

  <script>
    // Three buttons
    const req1 = document.querySelector('#req1')
    const cancel = document.querySelector('#cancel')
    const reset = document.querySelector('#reset')
    // Result display area
    const result = document.querySelector('#result')

    // Add click events to the three buttons
    req1.addEventListener('click'.async function () {
      const ret = await request('/req')
      result.textContent = ret
    })
    cancel.addEventListener('click'.function () {
      cancelReq()
    })
    reset.addEventListener('click'.function() {
      result.textContent = 'no result'
    })
  </script>
</body>

</html>

Copy the code

With that in mind, we can now begin to transform our case code.

The effect

As you can see, after the upgrade, the previous problem does not exist.

index.html

// Add the cancel function call where the HTTP request needs to be cancelled
tab3.addEventListener('click'.async function () {
  // Cancel the previous request
  cancelFn('Tab 2 interface request cancelled ')

  const { data } = await request({ url: '/tab3' })
  content.textContent = data
})
Copy the code

axiosRequest.js

In fact, Axios’ native solution is the same as the generic solution, which takes advantage of the immutable state after promises are made.

Original planAxios website

const baseURL = 'http://localhost:3000'
const CancelToken = axios.CancelToken

const ins = axios.create({
  baseURL,
  timeout: 10000
})

ins.interceptors.request.use(config= > {
  // Intercepting requests, where you can customize some configurations, such as tokens
  return config
})

ins.interceptors.response.use(response= > {
  // Intercept the response, according to the status code returned by the server to do some custom response and information prompt
  return response
})

// initialize as a function to prevent errors
let cancelFn = function () {}

function request(reqArgs) {
  // Set a cancelToken instance in the passed argument
  reqArgs.cancelToken = new CancelToken(function (cancel) {
    // Expose the cancel function outward
    cancelFn = cancel
  })
  return ins.request(reqArgs)
}
Copy the code

General plan

const baseURL = 'http://localhost:3000'
const CancelToken = axios.CancelToken

const ins = axios.create({
  baseURL,
  timeout: 10000
})

ins.interceptors.request.use(config= > {
  // Intercepting requests, where you can customize some configurations, such as tokens
  return config
})

ins.interceptors.response.use(response= > {
  // Intercept the response, according to the status code returned by the server to do some custom response and information prompt
  return response
})

// initialize as a function to prevent errors
let cancelFn = function () {}

function request(reqArgs) {
  return new Promise((resolve, reject) = > {
    // Request the interface
    ins.request(reqArgs).then(res= > resolve(res))

    // Expose the cancel function outward
    cancelFn = function (msg) {
      reject({ message: msg })
    }
  })
}

Copy the code

fetchRequest.js

Fetch the API support through AbortController/AbortSignal interrupt request, also can use the generic solution. The generic solution is better because the interrupted Fetch is marked and becomes unavailable unless the page is refreshed. In fact, fetch’s native solution does not solve the problem in our case. It interrupts the request, but it also prevents subsequent requests from being sent.

Original plan

After execution, you will see the following information on the console:

The first indicates that the user has terminated the FETCH request, which is the prompt generated by the cancelFn function call. The second error message is caused by another fetch request (clicking TAB 3 button) after we interrupt the fetch request. It tells you that the fetch on the current window object has been terminated by the user, so you need to refresh the page. Reinitialize these global objects (window.fetch)

/ / by AbortController/AbortSignal interrupt request
const abortController = new AbortController()

// Expose the cancel function outward
function cancelFn() {
  // Interrupt all network traffic, especially if you want to stop transmission of large loads
  abortController.abort()
}

const baseURL = 'http://localhost:3000'

function request(reqArgs) {
  // The data format returned by the interface is designed to be compatible with axiOS sample code
  return fetch(baseURL + reqArgs.url, { signal: abortController.signal }).then(async response => ({ data: await response.json() }))
}

Copy the code

General plan

// initialize as a function to prevent errors
let cancelFn = function () {}

const baseURL = 'http://localhost:3000'

function request(reqArgs) {
  return new Promise((resolve, reject) = > {
    // The data format returned by the interface is designed to be compatible with axiOS sample code
    fetch(baseURL + reqArgs.url).then(async response => resolve({ data: await response.json() }))

    // Expose the cancel function outward
    cancelFn = function(msg) {
      reject({ message: msg })
    }
  })
}

Copy the code

xhrRequest.js

Original plan

const baseURL = 'http://localhost:3000'

const xhr = new XMLHttpRequest()
function request(reqArgs) {
  return new Promise((resolve, reject) = > {
    xhr.onload = function () {
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
        // The data format returned by the interface is designed to be compatible with axiOS sample code
        resolve({ data: xhr.responseText })
      } else {
        / / make a mistake
        reject(xhr.status)
      }
    }
    xhr.open(reqArgs.method || 'get', baseURL + reqArgs.url, true)
    xhr.send(reqArgs.data || null)})}// Expose the cancel function outward
function cancelFn() {
  // The XHR native provides abort
  xhr.abort()
}

Copy the code

General plan

const baseURL = 'http://localhost:3000'

const xhr = new XMLHttpRequest()

// Initialize the cancel function to prevent error calls
let cancelFn = function() {}

function request(reqArgs) {
  return new Promise((resolve, reject) = > {
    xhr.onload = function () {
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
        // The data format returned by the interface is designed to be compatible with axiOS sample code
        resolve({ data: xhr.responseText })
      } else {
        / / make a mistake
        reject(xhr.status)
      }
    }
    xhr.open(reqArgs.method || 'get', baseURL + reqArgs.url, true)
    xhr.send(reqArgs.data || null)

    // Expose the cancel function outward
    cancelFn = function (msg) {
      reject({ message: msg })
    }
  })
}

Copy the code

conclusion

This is all the scenarios for terminating asynchronous HTTP requests, which can be summarized in two categories:

  • Original plan

  • General scheme for secondary encapsulation based on Promise

You can choose according to your own needs.

link

Your application needs a video version of the Cancelable asynchronous HTTP request module

github