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