Author: valentinogagliardi

Translator: Front-end wisdom

Source: making


Everyone said there was no project on your resume, so I found one and gave it away【 Construction tutorial 】.

REST API and XMLHttpRequest

JS is pretty strong, if you ask me. As a scripting language that runs in a browser, it can do all these things:

  • Dynamically creating elements
  • Add interactivity

And so on. In Chapter 8, we built an HTML table from arrays. A hard-coded array is a synchronous data source, which means it can be used directly in our code without waiting. But most of the time, data is requested from the background. Network requests are always asynchronous operations, not synchronous data sources: data is requested, and the server responds with some delay.

JS itself has no built-in asynchrony: it is a “hosted” environment (browser or Node.j) that provides external help for handling time-consuming operations. In Chapter 3, we looked at setTimeout and setInterval, which belong to the Web API. Browsers provide a number of apis, including one called XMLHttpRequest for web requests.

In fact, it comes from the days of XML data format. JSON is now the most popular communication “protocol” for moving data between Web services, but the name XMLHttpRequest eventually stuck.

XMLHttpRequest, which stands for “Asynchronous JavaScript and XML,” is also part of AJAX technology. AJAX was born to handle web requests as flexibly as possible in the browser. Its purpose is to be able to fetch data from a remote data source without causing a page refresh. The idea was almost revolutionary at the time. With the introduction of XMLHttpRequest (about 13 years ago), we can use it for asynchronous requests.

var request = new XMLHttpRequest();

request.open('GET', "https://academy.valentinog.com/api/link/");

request.addEventListener('load', function() {
  console.log(this.response);
})

request.send();
Copy the code

In the example above:

  • Create a new XMLHttpRequest object

  • Open the request by providing a method and url

  • Register event listeners

  • Send the request

XMLHttpRequest is based on DOM events. We can use addEventListener or onLoad to listen for the “Load” event, which is triggered when the request succeeds. For failed requests (network errors), we can register a listener on the “error” event:

var request = new XMLHttpRequest(); request.open("GET", "https://academy.valentinog.com/api/link/") request.onload = function() { console.log(this.response) } request.onerror = Function () {// handle error} request.send();Copy the code

Armed with this knowledge, let’s make better use of XMLHttpRequest.

Build an HTML list by requesting data with XMLHttpRequest

After extracting the data from the REST API, we’ll build a simple HTML list. Create a new file called build-list.html:

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>XMLHttpRequest</title> </head> <body> </body> <script src="xhr.js"></script> </html>Copy the code

Next, create a file named xhr.js in the same folder. In this file, create a new XHR request:

"use strict";

const request = new XMLHttpRequest();
Copy the code

The above call (constructor style) creates a new object of type XMLHttpRequest. In contrast to asynchronous functions such as setTimeout, we take callbacks as arguments:

setTimeout(callback, 10000); function callback() { console.log("hello timer!" ); }Copy the code

XMLHttpRequest is based on a DOM event, and the handler callback is registered on the onLoad object. The LOAD event fires when the request is successful.

"use strict"; const request = new XMLHttpRequest(); request.onload = callback; function callback() { console.log("Got the response!" ); }Copy the code

After registering the callback, we can use open() to open the request. It accepts an HTTP method

"use strict"; const request = new XMLHttpRequest(); request.onload = callback; function callback() { console.log("Got the response!" ); } request.open("GET", "https://academy.valentinog.com/api/link/");Copy the code

Finally, we can use send() to send the actual request

"use strict"; const request = new XMLHttpRequest(); request.onload = callback; function callback() { console.log("Got the response!" ); } request.open("GET", "https://academy.valentinog.com/api/link/"); request.send();Copy the code

Open build-list. HTML in your browser and see “Got the response!” in the console. “Indicates that the request is successful. If you remember from Chapter 6, every regular JS function has a reference to its host object. Because the callback runs in an XMLHttpRequest object, you can get the data returned by the server through this.Response.

"use strict";

const request = new XMLHttpRequest();

request.onload = callback;

function callback() {
  // this refers to the new XMLHttpRequest
  // response is the server's response
  console.log(this.response);
}

request.open("GET", "https://academy.valentinog.com/api/link/");
request.send();
Copy the code

Save the file and refresh build-list.html. The returned data is visible on the console in the form of a string, and there are two ways to convert it to JSON:

  • Method 1: Configure the response type on the XMLHttpRequest object

  • Method 2: Use json.parse ()

Method one:

"use strict";

const request = new XMLHttpRequest();

request.onload = callback;

function callback() {
  // this refers to the new XMLHttpRequest
  // response is the server's response
  console.log(this.response);
}

// configure the response type
request.responseType = "json";
//

request.open("GET", "https://academy.valentinog.com/api/link/");
request.send();
Copy the code

Method two is recommended, which is also in line with our current programming habits:

"use strict";

const request = new XMLHttpRequest();

request.onload = callback;

function callback() {
  const response = JSON.parse(this.response);
  console.log(response);
}

request.open("GET", "https://academy.valentinog.com/api/link/");
request.send();
Copy the code

Refresh build-list.html again and you’ll see an array of JS objects, each with the same structure:

[
  //
  {
    title:
      "JavaScript Engines: From Call Stack to Promise, (almost) Everything You Need to Know",
    url: "https://www.valentinog.com/blog/engines/",
    tags: ["javascript", "v8"],
    id: 3
  }
  //
]
Copy the code

This time, instead of creating the array manually as we did in Chapter 8, we request the data through the REST API.

Building HTML lists (and debug classes) with JS

Here we build using ES6 class methods and use private class fields (which, at the time of writing, Are not supported by Firefox). Before writing any code, ask yourself, “How will someone use my class?” For example, another developer could use our code and call the class by passing it in:

  • The URL used to get the data

  • The HTML element to which the list is to be attached

    const url = “academy.valentinog.com/api/link/”; const target = document.body; const list = new List(url, target);

With these requirements, we are ready to start writing our class code. For now, it should take two arguments in the constructor and have a method to get the data

class List { constructor(url, target) { this.url = url; this.target = target; } getData() { return "stuff"; }}Copy the code

The general view in software development is that class members and methods cannot be accessed externally unless there is a good reason to do the opposite. In JS, there is no native way to hide methods and variables unless you use modules (Chapter 2). Even classes are not immune to information leakage, but with private fields, there is a high probability of avoiding such problems. The JS private class field is not yet standard, but most browsers already support it. It is represented by #, overriding the above class:

class List { #url; #target; constructor(url, target) { this.#url = url; this.#target = target; } getData() { return "stuff"; }}Copy the code

You may not like the syntax, but private class fields do the job. In this way, we cannot access urls and targets externally:

class List {
  #url;
  #target;

  constructor(url, target) {
    this.#url = url;
    this.#target = target;
  }

  getData() {
    return "stuff";
  }
}

const url = "https://academy.valentinog.com/api/link/";
const target = document.body;
const list = new List(url, target);

console.log(list.url); // undefined
console.log(list.target); // undefined
Copy the code

With this structure, we can move the data retrieval logic to getData.

"use strict";

class List {
  #url;
  #target;

  constructor(url, target) {
    this.#url = url;
    this.#target = target;
  }

  getData() {
    const request = new XMLHttpRequest();
    request.onload = function() {
      const response = JSON.parse(this.response);
      console.log(response);
    };

    request.open("GET", this.#url);
    request.send();
  }
}

const url = "https://academy.valentinog.com/api/link/";
const target = document.body;
const list = new List(url, target);
Copy the code

Now, to display the data, let’s add a method called Render after getData. Render will create an HTML list for us, starting with the array passed as an argument:

"use strict"; class List { #url; #target; constructor(url, target) { this.#url = url; this.#target = target; } getData() { const request = new XMLHttpRequest(); request.onload = function() { const response = JSON.parse(this.response); console.log(response); }; request.open("GET", this.#url); request.send(); } // The new method render(data) { const ul = document.createElement("ul"); for (const element of data) { const li = document.createElement("li"); const title = document.createTextNode(element.title); li.appendChild(title); ul.appendChild(li); } this.#target.appendChild(ul); }}Copy the code

Notice document.createElement(), document.createTextNode() and appendChild(). We saw this in Chapter 8 when we looked at DOM manipulation. The this.#target private field appends the HTML list to the DOM. Now, I think:

  • Get the JSON response and call Render

  • GetData is called immediately when the user creates a new list “instance.

To do this, we call render inside the Request. onload callback:

getData() {
  const request = new XMLHttpRequest();
  request.onload = function() {
    const response = JSON.parse(this.response);
    // Call render after getting the response
    this.render(response);
  };

  request.open("GET", this.#url);
  request.send();
}
Copy the code

GetData, on the other hand, should run in a constructor:

constructor(url, target) {
  this.#url = url;
  this.#target = target;
  // Call getData as soon as the class is used
  this.getData();
}
Copy the code

Complete code:

"use strict";

class List {
  #url;
  #target;

  constructor(url, target) {
    this.#url = url;
    this.#target = target;
    this.getData();
  }

  getData() {
    const request = new XMLHttpRequest();
    request.onload = function() {
      const response = JSON.parse(this.response);
      this.render(response);
    };

    request.open("GET", this.#url);
    request.send();
  }

  render(data) {
    const ul = document.createElement("ul");
    for (const element of data) {
      const li = document.createElement("li");
      const title = document.createTextNode(element.title);
      li.appendChild(title);
      ul.appendChild(li);
    }
    this.#target.appendChild(ul);
  }
}

const url = "https://academy.valentinog.com/api/link/";
const target = document.body;
const list = new List(url, target);
Copy the code

Try this: Refresh build-list.html in your browser and view the console

Uncaught TypeError: this.render is not a function
Copy the code

This.render is not a function! What could it be? At this point, you might want to get to Chapter 6 or later, where you can debug your code. After this.render(response) in getData, add the debugger instruction:

getData() {
  const request = new XMLHttpRequest();
  request.onload = function() {
    const response = JSON.parse(this.response);
    debugger;
    this.render(response);
};

request.open("GET", this.#url);
  request.send();
}
Copy the code

The debugger adds what is called a breakpoint where execution will stop. Now open your browser console and refresh build-list.html. Here’s what you’ll see in Chrome:

Look carefully at the Scopes TAB. GetData does have a this, but it points to XMLHttpRequest. In other words, we’re trying to access this.render on the wrong object.

Why does this not match? This is because the callback passed to Request.onload runs in a host object of type XMLHttpRequest, the result of calling const Request = request = new XMLHttpRequest(). The solution, as mentioned in previous chapters, is to use the arrow function.

  getData() {
    const request = new XMLHttpRequest();
    // The arrow function in action
    request.onload = () => {
      const response = JSON.parse(this.response);
      debugger;
      this.render(response);
    };

    request.open("GET", this.#url);
    request.send();
  }
Copy the code

Refresh build-list.html and examine it

Uncaught SyntaxError: Unexpected token u in JSON at position 0
Copy the code

Well, the previous error is gone, but json.parse now has a problem. It’s easy to imagine that it has to do with this. Move the debugger up one line

  getData() {
    const request = new XMLHttpRequest();
    request.onload = () => {
      debugger;
      const response = JSON.parse(this.response);
      this.render(response);
    };

    request.open("GET", this.#url);
    request.send();
  }
    
Copy the code

Refresh build-list. HTML and view the Scopes again in the browser console. Response is undefined, because this we’re accessing is a List. This is consistent with the behavior of arrow functions and classes (classes default to strict mode). So is there a solution?

As you learned from Chapter 8 DOM and Events, each callback passed as an event listener has access to the Event object. There is also a property in the event object called Target that points to the object that triggered the event. You can use the event.target.response command to obtain the data returned by the response.

 getData() {
    const request = new XMLHttpRequest();
    request.onload = event => {
      const response = JSON.parse(event.target.response);
      this.render(response);
    };

    request.open("GET", this.#url);
    request.send();
  }
Copy the code

Complete code:

"use strict";

class List {
  #url;
  #target;

  constructor(url, target) {
    this.#url = url;
    this.#target = target;
    this.getData();
  }

  getData() {
    const request = new XMLHttpRequest();
    request.onload = event => {
      const response = JSON.parse(event.target.response);
      this.render(response);
    };

    request.open("GET", this.#url);
    request.send();
  }

  render(data) {
    const ul = document.createElement("ul");
    for (const element of data) {
      const li = document.createElement("li");
      const title = document.createTextNode(element.title);
      li.appendChild(title);
      ul.appendChild(li);
    }
    this.#target.appendChild(ul);
  }
}

const url = "https://academy.valentinog.com/api/link/";
const target = document.body;
const list = new List(url, target);    
Copy the code

Next, explore the evolution of XMLHttpRequest: Fetch.

Asynchronous evolution: From XMLHttpRequest to Fetch

The Fetch API, a native browser method for making AJAX requests, is often overlooked by libraries such as Axios. Fetch was born in 2015 along with ES6 and the new Promise object.

AJAX, on the other hand, has had a technique for retrieving data in a browser since 1999. Nowadays we take AJAX and Fetch for granted, but few people know that Fetch is nothing more than a “glorified version” of XMLHttpRequest. Fetch is much cleaner than a typical XMLHttpRequest request and, more importantly, is based on promises. Here’s a simple example:

fetch("https://academy.valentinog.com/api/link/").then(function(response) {
  console.log(response);
});
Copy the code

If you run it in a browser, the console prints a response object. Depending on the content type of the request, the data needs to be converted to JSON when it is returned

fetch("https://academy.valentinog.com/api/link/").then(function(response) {
  return response.json();
}); 
Copy the code

Contrary to what you might think, just calling doesn’t return the actual data. Since Response.json () also returns a Promise, it takes a step further to get the JSON payload:

fetch("https://academy.valentinog.com/api/link/")
  .then(function(response) {
    return response.json();
  })
  .then(function(json) {
    console.log(json);
  });
Copy the code

Fetch is more convenient and cleaner than XMLHttpRequest, but it has many features. For example, special care must be taken to check for errors in the response. In the next section, we’ll learn more about it and rebuild Fetch from scratch.

Rebuild the Fetch API from scratch

To better understand the Fetch principle, let’s rewrite the Fetch method. First, create a new file called fetch. HTML with the following contents:

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Building Fetch from scratch</title> </head> <body> </body> <script src="fetch.js"></script> </html>Copy the code

Then create another file called fetch. Js in the same folder with the following contents:

"use strict";

window.fetch = null;
Copy the code

In the first line, we make sure we are in strict mode, and in the second line, we “cancel” the original Fetch API. Now we can start building our Fetch API. The way FETCH works is very simple. It accepts a URL and makes a GET request against it:

fetch("https://academy.valentinog.com/api/link/").then(function(response) {
  console.log(response);
});
Copy the code

When a function with THEN says that the function is “chain-able,” it means that it returns a Promise. So, in fetch. Js, we create a function called fetch that accepts a URL and returns a new Promise. To create a Promise, call the Promise constructor and pass in a callback function to parse and reject:

function fetch(url) {
  return new Promise(function(resolve, reject) {
    // do stuff
  });
}
Copy the code

Code improvement:

"use strict"; window.fetch = fetch; function fetch(url) { return new Promise(function(resolve, reject) { resolve("Fake response!" ); }); } fetch("https://academy.valentinog.com/api/link/").then(function(response) { console.log(response); });Copy the code

Get “Fake Response!” in the console. . Of course, this is still a useless fetch because nothing is returned from the API. Let’s implement the real behavior with the help of XMLHttpRequest. We already know how XMLHttpRequest creates a request. Next, wrap the XMLHttpRequest into our Promise

function fetch(url) { return new Promise(function(resolve, reject) { const request = new XMLHttpRequest(); request.open("GET", url); request.onload = function() { resolve(this.response); }; request.onerror = function() { reject("Network error!" ); }; request.send(); }); }Copy the code

Rejected promises are handled by catches:

fetch("https://acdemy.valentinog.com/api/link/")
  .then(function(response) {
    console.log(response);
  })
  .catch(function(error) {
    console.log(error);
  });
Copy the code

Now, if the URL is wrong, a specific error message is printed to the console. If the URL is correct, print the request to the data:

The above implementation method is not perfect. First, we need to implement a function that returns JSON. The actual Fetch API generates a response that can later be converted to JSON, BLOb, or text, as shown below. (For the scope of this exercise, we will only implement JSON functions.)

fetch("https://academy.valentinog.com/api/link/")
  .then(function(response) {
    return response.json();
  })
  .then(function(json) {
    console.log(json);
  })
Copy the code

Implementing this feature should be easy. It seems likely that “response” is a separate entity with a JSON () function. The JS prototype system is great for building code (see Chapter 5). Let’s create a constructor called Response and a method bound to its prototype (in fetch. Js) :

function Response(response) {
  this.response = response;
}

Response.prototype.json = function() {
  return JSON.parse(this.response);
};
Copy the code

So, we can use Response in Fetch:

window.fetch = fetch; function Response(response) { this.response = response; } Response.prototype.json = function() { return JSON.parse(this.response); }; function fetch(url) { return new Promise(function(resolve, reject) { const request = new XMLHttpRequest(); request.open("GET", url); Request. Onload = function() {// front: // resolve(this.response); // Now: const response = new response (this.response); resolve(response); }; request.onerror = function() { reject("Network error!" ); }; request.send(); }); } fetch("https://academy.valentinog.com/api/link/") .then(function(response) { return response.json(); }) .then(function(json) { console.log(json); }) .catch(function(error) { console.log(error); });Copy the code

The code above prints an array of objects in the browser console. Now let’s deal with the error. The real version of Fetch is much more complex than our Polyfill, but it’s not difficult to implement the same behavior. The response object in Fetch has one property, a Boolean value named ** “OK” **. This Boolean value is set to true on success and false on failure. Developers can check booleans and change the Promise handler by throwing an error. Here’s how to check the state using an actual Fetch:

fetch("https://academy.valentinog.com/api/link/") .then(function(response) { if (! response.ok) { throw Error(response.statusText); } return response.json(); }) .then(function(json) { console.log(json); }) .catch(function(error) { console.log(error); });Copy the code

As you can see, there’s also a “statusText”. “Ok” and “statusText” seem easy to implement in Response objects. If the server responds successfully, response.ok is true:

  • The status code is equal to or less than 200
  • The status code is smaller than 300

Reconstruct the Response method, as shown below:


function Response(response) {
  this.response = response.response;
  this.ok = response.status >= 200 && response.status < 300;
  this.statusText = response.statusText;
}

Copy the code

There is no need to create “statusText” because it is already returned from the original XMLHttpRequest response object. This means that you only need to pass the entire response when constructing a custom response

function fetch(url) { return new Promise(function(resolve, reject) { const request = new XMLHttpRequest(); request.open("GET", url); Request. Onload = function() {// 前 : // var response = new response (this.response); // Now: pass the entire response const response = new response (this); resolve(response); }; request.onerror = function() { reject("Network error!" ); }; request.send(); }); }Copy the code

But now there is something wrong with our polyfill. It accepts a single parameter “URL” and only makes a GET request on it. Fixing this should be easy. First, we can accept a second parameter called requestInit. We can then construct a new request object from this argument:

  • By default, GET requests are made
  • If provided, it is used in requestInitbody,methodheaders

First, create a new Request function with some attributes named body, method, and headers, as follows:

function Request(requestInit) {
  this.method = requestInit.method || "GET";
  this.body = requestInit.body;
  this.headers = requestInit.headers;
}
Copy the code

But on top of that, we can add a minimal logic to set the request header

function fetch(url, requestInit) { return new Promise(function(resolve, reject) { const request = new XMLHttpRequest(); const requestConfiguration = new Request(requestInit || {}); request.open(requestConfiguration.method, url); request.onload = function() { const response = new Response(this); resolve(response); }; request.onerror = function() { reject("Network error!" ); }; / / set the headers for (const header in requestConfiguration. Headers) {request. SetRequestHeader (header, requestConfiguration.headers[header]); } // more soon }); }Copy the code

SetRequestHeader can be used directly on the XMLHttpRequest object. Headers is important for configuring AJAX requests. Most of the time, you’ll probably want to set application/ JSON or authentication tokens in HEADERS.

  • If there is no body, it is an empty request
  • A request with some payload isbodyTo provide the
function fetch(url, requestInit) { return new Promise(function(resolve, reject) { const request = new XMLHttpRequest(); const requestConfiguration = new Request(requestInit || {}); request.open(requestConfiguration.method, url); request.onload = function() { const response = new Response(this); resolve(response); }; request.onerror = function() { reject("Network error!" ); }; // Set headers on the request for (const header in requestConfiguration.headers) { request.setRequestHeader(header, requestConfiguration.headers\[header\]); } // If there's a body send it // If not send an empty GET request requestConfiguration.body && request.send(requestConfiguration.body); requestConfiguration.body || request.send(); }); }Copy the code

Here is the complete code:

"use strict"; window.fetch = fetch; function Response(response) { this.response = response.response; this.ok = response.status >= 200 && response.status < 300; this.statusText = response.statusText; } Response.prototype.json = function() { return JSON.parse(this.response); }; function Request(requestInit) { this.method = requestInit.method || "GET"; this.body = requestInit.body; this.headers = requestInit.headers; } function fetch(url, requestInit) { return new Promise(function(resolve, reject) { const request = new XMLHttpRequest(); const requestConfiguration = new Request(requestInit || {}); request.open(requestConfiguration.method, url); request.onload = function() { const response = new Response(this); resolve(response); }; request.onerror = function() { reject("Network error!" ); }; for (const header in requestConfiguration.headers) { request.setRequestHeader(header, requestConfiguration.headers[header]); } requestConfiguration.body && request.send(requestConfiguration.body); requestConfiguration.body || request.send(); }); } const link = { title: "Building a Fetch Polyfill From Scratch", url: "https://www.valentinog.com/fetch-api/" }; const requestInit = { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(link) }; fetch("https://academy.valentinog.com/api/link/create/", requestInit) .then(function(response) { if (! response.ok) { throw Error(response.statusText); } return response.json(); }) .then(function(json) { console.log(json); }) .catch(function(error) { console.log(error); });Copy the code

The real Fetch API implementation is much more complex and supports advanced features. We’ve only scratched the surface. You can improve the code so, for example, that the logic to add HEADERS can exist independently of the method.

In addition, there is plenty of room to add new features: support for PUT and DELETE, and more functions that return responses in different formats. If you want to see a more sophisticated get API polyfill, check out whatWG-Fetch from Github’s engineers. You will find many similarities with our Polyfill.

conclusion

AJAX has changed the way we build the Web by giving us the opportunity to build smooth, user-friendly interfaces. Gone are the days of the classic page refresh.

Now we can build elegant JS applications and get the data we need in the background. XMLHttpRequest is a good old legacy API for making HTTP requests that is still in use today, but in a different form: the Fetch API.

The bugs that may exist after code deployment cannot be known in real time. In order to solve these bugs, I spent a lot of time on log debugging. Incidentally, I recommend a good BUG monitoring tool for youFundebug.

Original text: github.com/valentinoga…

communication

This article is updated every week, you can search wechat “big move the world” for the first time to read and urge more (one or two earlier than the blog hey), this article GitHub github.com/qq449245884… It has been included and sorted out a lot of my documents. Welcome Star and perfect. You can refer to the examination points for review in the interview.