Sending HTTP requests has never been an easy thing in front-end engineering. There are scary ActiveXObject and awkward XMLHttpRequest API design before, and even the use of these native APIS is still one of the test points of many big companies’ front-end school.
Just like this, the appearance of FETCH in the front circle aroused a great deal of excitement, and everyone was jubilant and eager to kill all $. Ajax in the project immediately. However, after the novelty, is fetch really as good as you imagined?
If you’re not familiar with FETCH, check out my colleague @Camsong’s 2015 article, “Traditional Ajax is Dead, Fetch Lives forever.”
Fetch is a low-level API, and it’s not meant to encapsulate functionality or implementations like you’re used to with libraries like $. Ajax or Axios. Because of this positioning, you will encounter a lot of frustration when learning or using the FETCH API.
(For those of you who don’t have the patience to read the full article, please remember that the main point of this article is not to criticize FETCH, in fact, the advent of FETCH is definitely an advance in the front-end domain. Focus on the highlighted parts as long as you understand the main idea.)
Sending requests is more complicated than you think
Many people will be drawn to fetch’s concise API at first sight:
fetch('http://abc.com/tiger.png');
Copy the code
What used to take ten lines of code like new XMLHttpRequest can now be done in one line of code.
But when you’re actually using fetch in a project, you need to send data to the server. How many lines of code does fetch take to send an object to the server? (For compatibility, most projects will use Application/X-www-form-urlencoded content-Type when sending POST requests)
Let’s see how this works with jQuery:
$.post('/api/add', {name: 'test'});
Copy the code
Then look at how fetch handles it:
fetch('/api/add', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: Object.keys({name: 'test'}).map((key) => { return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); }).join('&') });Copy the code
Wait, what’s that long string of code for the body field doing? Since fetch is a low-level API, you need to encode the payload of the HTTP request yourself and specify the Content-Type field in the HTTP Header yourself.
Is that the end of it? If you send a POST request this way in your own project, you will likely get 401 Unauthorized results (depending on how your server handles the Unauthorized situation). If you take a closer look at the documentation, fetch does not send requests with cookies by default!
Ok, let’s make fetch take a Cookie:
fetch('/api/add', {
method: 'POST',
credentials: 'include',
...
});
Copy the code
In this way, a basic POST request can be sent.
Similarly, if you need to POST JSON to the server, you need to do this:
fetch('/api/add', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json; charset=UTF-8' }, body: JSON.stringify({name: 'test'}) });Copy the code
Isn’t ajax a little more complicated than $. Ajax encapsulation?
Error handling is more complicated than you think
Fetch is supposed to be a Promise based API. Each FETCH request returns a Promise object, and Promise exception handling, whether convenient or not, is at least familiar. However, fetch has many tricks to handle exceptions.
Suppose we use fetch to request a resource that does not exist:
fetch('xx.png')
.then(() => {
console.log('ok');
})
.catch(() => {
console.log('error');
});
Copy the code
Console is supposed to print “error”, but what happens? There is a picture and there is a truth:
Why is “OK” printed?
In accordance with the MDNclaimFetch only rejects the promise when a network error occurs, such as a user disconnecting from the Internet or the domain name of the address requested cannot be resolved. As long as the server can return an HTTP response (or even just an OPTIONS response from CORS PreFlight), the Promise must be resolved.
So how do you determine if a fetch request is successful? You should use the response.ok field:
fetch('xx.png')
.then((response) => {
if (response.ok) {
console.log('ok');
} else {
console.log('error');
}
})
.catch(() => {
console.log('error');
});
Copy the code
Execute again and finally see the correct log:
The Stream API is more complex than you might think
When your server returns data in JSON format, you expect the fetch to return you a normal JavaScript object, but instead you get oneResponse
Object, and the actual result of the request — i.eresponse.body
— is oneReadableStream
.
fetch('/api/user.json? {"name": "test", "age": 1} then((response) => {name: 'test', age: 1} 1} object return response.json(); Parse response.body to JS}). Then (data => {console.log(data); // {name: 'test', age: 1} });Copy the code
You might think that the technical details written in the specification are of no concern to the fetch user, but in practice you will encounter various problems that force you to know these details.
First of all, it should be acknowledged that Fetch’s design of Response.body as a ReadableStream is actually quite forward-looking and makes it very useful when you request large files. However, short JSON fragments are more common in our daily use. For compatibility with unusual designs, we have to call Response.json () one more time.
Not only is the call cumbersome, but if your server uses a strict REST style and for some special cases does not return a JSON string but uses an HTTP status code (e.g. 204 No Content), then response.json() will throw an exception.
In addition, Response limits repeated reading and conversion of the Response content, such as the following code:
var prevFetch = window.fetch; window.fetch = function() { prevFetch.apply(this, arguments) .then(response => { return new Promise((resolve, reject) => { response.json().then(data => { if (data.hasError === true) { tracker.log('API Error'); } resolve(response); }); }); }); } fetch('/api/user.json? id=1') .then(response => { return response.json(); }). Then (data => {console.log(data); });Copy the code
A simple AOP is done on fetch, attempting to intercept all the results of the request and logging the error interface if the hasError field in the returned JSON object is true.
However, such code causes the following error:
Uncaught TypeError: Already read
After some debugging, you’ll see that it’s because we’ve already called Response.json () in the section, and we’ll get an error if we call this method repeatedly. (In fact, calling any other conversion method again, such as.text(), will also return an error.)
Therefore, implementing AOP on FETCH requires a different approach.
Other problems
1. Fetch does not support synchronous requests
It is well known that synchronous requests block page interactions, but there are still quite a few projects that use synchronous requests, probably for historical architectural reasons and so on. This cannot be done if you switch the fetch.
2. Fetch does not support canceling a request
With XMLHttpRequest you can cancel a request with the xhr.abort() method (although this method is also unreliable and depends on the server’s implementation), but fetch does nothing, at least for now.
3. Fetch cannot view the progress of the request
With XMLHttpRequest you can dynamically update the progress of a request via the xhr.onProgress callback, something that fetch does not currently have native support for.
summary
Again, the fetch API has definitely pushed the front end forward in terms of sending requests.
However, it is important to realize that FETCH is a fairly low-level API that requires all sorts of wrapping and exception handling in real project use, not out of the box, let alone a direct replacement for $.ajax or other request libraries.
The resources
- fetch spec fetch.spec.whatwg.org/#body
- The fetch implementation github.com/github/fetc…
- What is Already a Read error stackoverflow.com/questions/3…
- Using the fetch HTTP request failed www.tjvantoll.com/2015/09/13/…
- Jakearchibald.com/2015/thats-…