Introduction to Ajax - Pro JavaScript Techniques, Second Edition (2015)

Pro JavaScript Techniques, Second Edition (2015)

8. Introduction to Ajax

John Resig1, Russ Ferguson1 and John Paxton1

(1)

NJ, United States

Ajax is a term coined by Jesse James Garrett of Adaptive Path to describe the asynchronous client-to-server communication that is made possible using the XMLHttpRequest object, which is provided by all modern browsers. An acronym for A synchronous JavaScript and XML, Ajax has evolved into a term used to encapsulate the techniques necessary to create a dynamic web application. Additionally, the individual components of the Ajax technique are completely interchangeable—using JSON instead of XML (for example) is perfectly valid.

Since the first edition of this book, usage of Ajax has changed significantly. Once an exotic API, Ajax is now a standard part of the professional JavaScript programmer's toolbox. The W3C has overhauled the XMLHttpRequest object, the foundation of Ajax, by the, adding new features and clarifying the behavior of other features. One of the core rules of Ajax: no connections to outside domains; this is enforced by using the Cross-Origin Resource Sharing standard, also known as CORS.

In this chapter, you’re going to see the details that make up the full Ajax process. We will concentrate on the XMLHttpRequest object API, but we’ll also discuss ancillary issues like handling responses, managing HTTP status codes, and so on. The intent is to provide you with a comprehensive understanding of what goes on within an Ajax request/response cycle.

We will not provide an API for Ajax interactions. Writing code to the various specifications governing Ajax is a straightforward affair, but writing a complete Ajax library for the real world is most assuredly not. Consider the Ajax portion of the jQuery library, which has over a dozen edge cases that handle various oddities about the API in Internet Explorer, Firefox, Chrome, and other browsers. Also, because jQuery, Dojo, Ext JS, and several other smaller libraries already have Ajax implementations, we see no reason to reinvent that particular wheel here. Instead, we will provide examples of Ajax interactions, written according to the current (as of publishing) specifications. These examples are intended to be instructive and demonstrative, not final. We encourage you when using Ajax to look into utility libraries like jQuery, Zepto, Dojo, Ext JS, and MooTools, or Ajax-focused libraries like Fermata and reqwest.

That leaves quite a bit to discuss! This chapter will cover the following:

· Examining the different types of HTTP requests

· Determining the best way to send data to a server

· Looking at the entire HTTP response and thinking about how to handle not only a successful response, but one that goes wrong (in some fashion) as well

· Reading, traversing, and manipulating the data result that comes from the server in its response

· Handling asynchronous responses

· Making requests across domains, enabled by CORS

Using Ajax

It doesn’t take much code to create a simple Ajax implementation; however, what the implementation affords you is great. For example, instead of having to force the user to request an entirely new web page after a form submission, your code can handle the submission process asynchronously, loading a small portion of desired results upon completion. In fact, when we tie Ajax requests to handlers for events like a keypress, there is no need to wait for the form submission at all. This is what is behind the “magic” of Google's autosuggest search function. When you start to enter a search term, Google fires an Ajax request based on your entry. As you refine your search, it sends out other Ajax requests. Google will display not only suggestions but, based on the first possible option, even a first page of results. Figure 8-1 shows an example of this process.

A978-1-4302-6392-0_8_Fig1_HTML.jpg

Figure 8-1.

An example of Instant Domain Search looking for domain names as you type

HTTP Requests

The most important and probably most consistent aspect of the Ajax process is the HTTPrequest portion. The Hypertext Transfer Protocol (HTTP) was designed simply to transfer HTML documents and associated files. Thankfully, all modern browsers support a means of establishing HTTP connections dynamically and asynchronously, using JavaScript. This proves to be incredibly useful in developing more responsive web applications.

Asynchronously sending data to the server and receiving additional data back is the ultimate purpose of Ajax. How the data is formatted ultimately depends on your specific requirements.

In the following sections, you will see how to format data to be transferred to a server using the different HTTP requests. You will then look at how to establish basic connections with the server, and you will see the details needed to make this happen in a cross-browser environment.

Establishing a Connection

All Ajax processes start with a connection to the server. Connections to the server are generally organized through the XMLHttpRequest object. (The lone exception is in older versions of Internet Explorer when making cross-domain requests. But we will cover that later on. For now, we shall rely upon the XMLHttpRequest object.)

Communication with the XMLHttpRequest object follows a lifecycle:

1.

Create an instance of XMLHttpRequest.

2.

Configure the object with appropriate settings.

3.

Open the request via a specific HTTP verb and destination.

4.

Send the request.

Listing 8-1 shows how to establish a basic GET request with the server.

Listing 8-1. A Cross-Browser Means of Establishing an HTTP GET Requestwith the Server

// Create the request object

var xml = new XMLHttpRequest();

// If we needed to do any special configuration, we would do it here

// Open the socket

xml.open('GET', '/some/url.cgi', true);

// Establish the connection to the server and send any additional data

xml.send();

The code needed to establish a connection with a server, as you can see, is quite simple; there really isn’t much to it. One set of difficulties arises when you want advanced features (such as checking for time-outs or modified data); we will cover those details in the “HTTP Response” section of this chapter. Another set of difficulties comes into play when you want to transfer data from the client (your browser) to the server. This is one of the most important features of the whole Ajax methodology. Will we send simple data on the URL? What about POSTed data? What about more complicated formats? With these questions (and others, of course) in mind, let’s look at the details needed to package some data and send it to a server.

Serializing Data

The first step of sending a set of data to a server is to format it so that the server can easily read it; this process is called serialization. We need to ask a few questions before serializing data. First:

1.

What data are we sending? Are we sending variable-and-value pairs? Large sets of data? Files?

2.

How are we sending this data, GET? POST? Another HTTP verb?

3.

What format of data are we using? There are two: application/x-www-form-urlencoded and multipart/form-data. The former is sometimes called query string encoding and takes the familiar form of var1=val1&var2=val2...

From a JavaScript perspective, the third question is the most important. The first and second questions are issues of design. They will have an effect on our application but will not necessarily require different code. But the question of which data format we use has a strong effect on our application.

In modern browsers, it is actually easier to deal with multipart/form-data information. Thanks to the FormData object, we can very easily serialize data into an object that our browser will automatically convert to the multipart/form-data format. Unfortunately, not all browsers support every option that is in the specification yet. But there is a lot we can do right now.

FormData Objects

FormData objects are a relatively new proposal covered by HTML5. The WHATWG and the W3C intended to give a more object-oriented, map-like approach to information sent as part of an Ajax (or really any HTTP) request. Accordingly, a FormData object can be either initialized as empty or associated with a form. If you are initializing with a form, get a reference to the containing form DOM element (usually via getElementById) and pass it into the FormData constructor. Otherwise, as stated, the FormData object will be empty. Either way, you can choose to add new data via the append function (Listing 8-2).

Listing 8-2. An Example Using the append method with FormData

// Create the formData object

var formDataObj= new FormData();

//append name/values to be sent to the server

formDataObj.append('first', 'Yakko');

formDataObj.append('second', 'Wakko');

formDataObj.append('third', 'Dot');

// Create the request object

var xml = new XMLHttpRequest();

// Set up a POST to the server

xml.open('POST', '/some/url.cgi');

// Send over the formData

xml.send(formDataObj);

There are some differences in the specifications, though. The WHATWG specification also includes functions for deleting, getting, and setting values on the object. None of the modern browsers implement these functions. In part, that’s because the W3C version of the specification has only the append function. The modern browsers follow this W3C spec, at least at the moment. This means that a FormData object is one-way: data goes in, but is only accessible on the other side of an HTTP request.

The alternative to FormData objects is to serialize in JavaScript. That is, take the data you intend to transfer to the server, URL-encode it, and send it to the server as part of the request. This is not too difficult, although there are some caveats to be cautious of.

Let’s take a look at some examples of the type of data that you can send to the server, along with their resulting server-friendly, serialized output, shown in Listing 8-3.

Listing 8-3. Examples of Raw JavaScript Objects Converted to Serialized Form

// A simple object holding key/value pairs

{

name: 'John',

last: 'Resig',

city: 'Cambridge',

zip: 02140

}

// Serialized form

name=John&last=Resig&city=Cambridge&zip=02140

// Another set of data, with multiple values

[

{ name: 'name', value: 'John' },

{ name: 'last', value: 'Resig' },

{ name: 'lang', value: 'JavaScript' },

{ name: 'lang', value: 'Perl' },

{ name: 'lang', value: 'Java' }

]

// And the serialized form of that data

name=John&last=Resig&lang=JavaScript&lang=Perl&lang=Java

// Finally, let's find some input elements

[

document.getElementById( 'name' ),

document.getElementById( 'last' ),

document.getElementById( 'username' ),

document.getElementById( 'password' )

]

// And serialize them into a data string

name=John&last=Resig&username=jeresig&password= test

The format that you’re using to serialize the data is the standard format for passing additional parameters in an HTTP request. You’re likely to have seen them in a standard HTTP GET request looking like this:

http://someurl.com/?name=John&last=Resig

This data can also be passed to a POST request (and in a much greater quantity than a simple GET). We will look at those differences in an upcoming section.

For now, let’s build a standard means of serializing the data structures presented in Listing 8-3. A function to do just that can be found in Listing 8-4. This function is capable of serializing most form input elements, with the exception of multiple-select inputs.

Listing 8-4. A Standard Function for Serializing Data Structures to an HTTP-Compatible Parameter Scheme

// Serialize a set of data. It can take two different types of objects:

// - An array of input elements.

// - A hash of key/value pairs

// The function returns a serialized string

function serialize(a) {

// The set of serialize results

var s = [];

// If an array was passed in, assume that it is an array

// of form elements

if ( a.constructor === Array ) {

// Serialize the form elements

for ( var i = 0; i < a.length; i++ )

s.push( a[i].name + '=' + encodeURIComponent( a[i].value ) );

// Otherwise, assume that it's an object of key/value pairs

} else {

// Serialize the key/values

for ( var j in a )

s.push( j + '=' + encodeURIComponent( a[j] ) );

}

// Return the resulting serialization

return s.join('&');

}

Now that there is a serialized form of your data (in a simple string), you can look at how to send that data to the server using a GET or a POST request.

Establishing a GET Request

Let’s revisit establishing an HTTP GET request with a server, using XMLHttpRequest, but this time sending along additional serialized data. Listing 8-5 shows a simple example of this.

Listing 8-5. A Cross-Browser Means of Establishing an HTTP GET Request with the Server (and Not Reading Any Resulting Data)

// Create the request object

var xml = new XMLHttpRequest();

// Open the asynchronous GET request

xml.open('GET', '/some/url.cgi?' + serialize( data ), true);

// Establish the connection to the server

xml.send();

The important part to note is that the serialized data is appended to the server URL (separated by a ? character). All web servers and application frameworks know to interpret the data included after the ? as a serialized set of key/value pairs.

Establishing a POST Request

The other way to establish an HTTP request with a server, using XMLHttpRequest, is with a POST, which involves a fundamentally different way of sending data to the server. Primarily, a POST request is capable of sending data of any format and of any length (not just limited to your serialized string of data).

The serialization format that you’ve been using for your data is generally given the content type application/x-www-form-urlencoded when passed to the server. This means that you could also send pure XML to the server (with a content type of text/xml or application/xml) or even a JavaScript object (using the content type application/json).

A simple example of establishing the request and sending additional serialized data appears in Listing 8-6.

Listing 8-6. A Cross-Browser Means of Establishing an HTTP POST Request with the Server (and Not Reading Any Resulting Data)

// Create the request object

var xml = new XMLHttpRequest();

// Open the asynchronous POST request

xml.open('POST', '/some/url.cgi', true);

// Set the content-type header, so that the server

// knows how to interpret the data that we're sending

xml.setRequestHeader(

'Content-Type', 'application/x-www-form-urlencoded');

// Establish the connection to the server and send the serialized data

xml.send( serialize( data ) );

To expand on the previous point, let’s look at a case of sending data that is not in your serialized format to the server. Listing 8-7 shows an example.

Listing 8-7. An Example of POSTing XML Data to a Server

// Create the request object

var xml = new XMLHttpRequest();

// Open the asynchronous POST request

xml.open('POST', '/some/url.cgi', true);

// Set the content-type header, so that the server

// knows how to interpret the XML data that we're sending

xml.setRequestHeader( 'Content-Type', 'text/xml');

// Establish the connection to the server and send the serialized data

xml.send( '<items><item id='one'/><item id='two'/></items>' );

The ability to send bulk amounts of data (there is no limit on the amount of data that you can send; by contrast, a GET request maxes out at just a couple KB of data, depending on the browser) is extremely important. With it, you can create implementations of different communication protocols, such as XML-RPC or SOAP.

This discussion, however, for simplicity is limited to some of the most common and useful data formats that can be made available as an HTTP response.

HTTP Response

Level 2 of the XMLHttpRequest class now provides better control over telling the browser how we want our data back. We do this by setting the responseType property and receive the requested data using the response property.

To start, let’s look at a very naïve example of processing the data from a server response, as shown in Listing 8-8.

Listing 8-8. Establishing a Connection with a Server and Reading the Resulting Data

// Create the request object

var request = new XMLHttpRequest();

// Open the asynchronous POST request

request.open('GET', '/some/image.png', true);

//Blob is a Binary Large Object

request.responseType = 'blob';

request.addEventListener('load', downloadFinished, false);

function downloadFinished(evt){

if(this.status == 200){

var blob = new Blob([this.response], {type: 'img/png'});

}

}

In this example you can see how to receive binary data and convert it into a PNG file. The responseType property can be set to any of the following:

· Text: Results return as a string of text

· ArrayBuffer: Results return as an array of binary data

· Document: Results are assumed to be a XML document, but it could be an HTML document

· Blob: Results return as a file like object of raw data

· JSON: Results return as a JSON document

Now that we know how to set the responseType, we can look at how to monitor the progress of our request.

Monitoring Progress

As we have seen before, using addEventListener makes our code easy to read and very flexible. Here we use the same technique on our request object. No matter whether you are downloading data from the server or uploading to it, you can listen for these events as shown in Listing 8-9.

Listing 8-9. Using addEventListener to Listen for Progress on a Request from the Server

var myRequest = new XMLHttpRequest();

myRequest.addEventListener('loadstart', onLoadStart, false);

myRequest.addEventListener('progress', onProgress, false);

myRequest.addEventListener('load', onLoad, false);

myRequest.addEventListener('error', onError, false);

myRequest.addEventListener('abort', onAbort, false);

//Must add eventListeners before running a send or open method

myRequest.open('GET', '/fileOnServer');

function onLoadStart(evt){

console.log('starting the request');

}

function onProgress(evt){

var currentPercent = (evt.loaded / evt.total) * 100;

console.log(currentPercent);

}

function onLoad(evt){

console.log('transfer is complete');

}

function onError(evt){

console.log('error during transfer');

}

function onAbort(evt){

console.log('the user aborted the transfer');

}

You can now understand a lot more about what is going on with your file than you could before. Using the loaded and total properties you might work out the percentage of the file being downloaded. If for some reason the user decided to stop the download, you would receive the abort event. If there was something wrong with the file, or if it had finished loading, you would receive either the error or the load event. Finally, when you first make the request of the server, you would receive the loadstart event. Now let’s take a quick look at timeouts.

Checking for Time-Outs and Cross-Origin Resource Sharing

Simply put, time-outs let you set a time for how long the application should wait until it stops looking for a response from the server. It is easy to set a time-out and listen for it.

Listing 8-10 shows how you would go about checking for a time-out in an application of your own.

Listing 8-10. An Example of Checking for a Request Time-Out

// Create the request object

var xml = new XMLHttpRequest();

// We're going to wait for a request for 5 seconds, before giving up

xml.timeout = 5000;

//Listen for the timeout event

xml.addEventListener('timeout', onTimeOut, false);

// Open the asynchronous POST request

xml.open('GET', '/some/url.cgi', true);

// Establish the connection to the server

xml.send();

By default, browsers will not allow applications to make request to servers other than the one the site originated from. This protects users from cross-site scripting attacks. The server must allow requests; otherwise, an INVALID_ACCESS error is given. The header given by the server would look like this:

Access-Control-Allow-Origin:*

//Using a wild card (*) to allow access from anyone.

Access-Control-Allow_origin: http://spaceappleyoshi.com

//Allowing from a certain domain

Summary

We now have a solid foundation to work with data on the server. We can tell the server what kind of results we expect back. We can also listen for events that will tell us things like the progress of the file transfer or if there was an error during the transfer. Finally, we discussed time-outs and cross-origin resource sharing or (CORS). In the next chapter we’ll take a look a few development tools for web production.