XMLHttp Requests for Ajax

Friday May 12th 2006 by Nicholas C. Zakas

Share:

Professional AJAX author Nicolas Zakas shows the benefits as well as some of the disadvantages of using HTTP requests in your applications.

When Microsoft Internet Explorer 5.0 introduced a rudimentary level of XML support, an ActiveX library called MSXML was also introduced. One of the objects provided in this library quickly became very popular: XMLHttp.

The XMLHttp object was created to enable developers to initiate HTTP requests from anywhere in an application. These requests were intended to return XML, so the XMLHttp object provided an easy way to access this information in the form of an XML document. Since it was an ActiveX control, XMLHttp could be used not only in web pages but also in any Windows-based desktop application; however, its popularity on the Web has far outpaced its popularity for desktop applications.

Picking up on that popularity, Mozilla duplicated the XMLHttp functionality for use in its browsers, such as Firefox. Shortly thereafter, both the Safari (as of version 1.2) and Opera (version 7.6) browsers had duplicated Mozilla's implementation. Today, all four browsers support XMLHttp to some extent. (Safari and Opera still have incomplete implementations, supporting GET and POST but no other request types.)

Creating an XMLHttp Object

The first step to using an XMLHttp object is, obviously, to create one. Because Microsoft's implementation is an ActiveX control, you must use the proprietary ActiveXObject class in JavaScript, passing in the XMLHttp control's signature:

var oXmlHttp = new ActiveXObject("Microsoft.XMLHttp");

This line creates the first version of the XMLHttp object (the one shipped with IE 5.0). The problem is that there have been several new versions released with each subsequent release of the MSXML library. Each release brings with it better stability and speed, so you want to make sure you are always using the most recent version available on the user's machine. The signatures are:

Microsoft.XMLHttp

MSXML2.XMLHttp

MSXML2.XMLHttp.3.0

MSXML2.XMLHttp.4.0

MSXML2.XMLHttp.5.0

Unfortunately, the only way to determine the best version to use is to try to create each one. Because this is an ActiveX control, any failure to create an object will throw an error, which means that you must enclose each attempt within a try...catch block. The end result is a function such as this:

The createXMLHttp() function stores an array of XMLHttp signatures, with the most recent one first. It iterates through this array and tries to create an XMLHttp object with each signature. If the creation fails, the catch statement prevents a JavaScript error from stopping execution; then the next signature is attempted. When an object is created, it is returned. If the function completes without creating an XMLHttp object, an error is thrown indicating that the creation failed.

Fortunately, creating an XMLHttp object is much easier in other browsers. Mozilla Firefox, Safari, and Opera all use the same code:

var oXmlHttp = new XMLHttpRequest();

Naturally, it helps to have a cross-browser way of creating XMLHttp objects. You can create such a function by altering the createXMLHttp() function defined previously:

Now this function first checks to see if an XMLHttpRequest class is defined (by using the typeof operator). If XMLHttpRequest is present, it is used to create the XMLHttp object; otherwise, it checks to see if the ActiveXObject class is present and, if so, goes through the same process of creating an XMLHttp object for IE. If both of these tests fail, an error is thrown.

The other option for creating cross-browser XMLHttp objects is to use a library that already has cross-browser code written. The zXml library, written by two of your authors, is one such library and is available for download at www.nczonline.net/downloads/. This library defines a single function for the creation of XMLHttp objects:

var oXmlHttp = zXmlHttp.createRequest();

The createRequest() function, and the zXml library itself, will be used throughout this book to aid in cross-browser handling of Ajax technologies.

After you have created an XMLHttp object, you are ready to start making HTTP requests from JavaScript. The first step is to call the open() method, which initializes the object. This method accepts the following three arguments:

Request Type: A string indicating the request type to be made-typically, GET or POST (these are the only ones currently supported by all browsers).

URL: A string indicating the URL to send the request to.

Async: A Boolean value indicating whether the request should be made asynchronously.

The last argument, async, is very important because it controls how JavaScript executes the request. When set to true, the request is sent asynchronously, and JavaScript code execution continues without waiting for the response; you must use an event handler to watch for the response to the request. If async is set to false, the request is sent synchronously, and JavaScript waits for a response from the server before continuing code execution. That means if the response takes a long time, the user cannot interact with the browser until the response has completed. For this reason, best practices around the development of Ajax applications favor the use of asynchronous requests for routine data retrieval, with synchronous requests reserved for short messages sent to and from the server.

To make an asynchronous GET request to info.txt, you would start by doing this:

Note that the case of the first argument, the request type, is irrelevant even though technically request types are defined as all uppercase.

Next, you need to define an onreadystatechange event handler. The XMLHttp object has a property called readyState that changes as the request goes through and the response is received. There are five possible values for readyState:

0 (Uninitialized): The object has been created but the open() method hasn't been called.

1 (Loading): The open() method has been called but the request hasn't been sent.

2 (Loaded): The request has been sent.

3 (Interactive): A partial response has been received.

4 (Complete): All data has been received and the connection has been closed.

Every time the readyState property changes from one value to another, the readystatechange event fires and the onreadystatechange event handler is called. Because of differences in browser implementations, the only reliable readyState values for cross-browser development are 0, 1, and 4. In most cases, however, you will check only for 4 to see when the request has returned:

The last step is to call the send() method, which actually sends the request. This method accepts a single argument, which is a string for the request body. If the request doesn't require a body (remember, a GET request doesn't), you must pass in null:

That's it! The request has been sent and when the response is received, an alert will be displayed. But just showing a message that the request has been received isn't very useful. The true power of XMLHttp is that you have access to the returned data, the response status, and the response headers.

To retrieve the data returned from the request, you can use the responseText or responseXML properties. The responseText property returns a string containing the response body, whereas the responseXML property is an XML document object used only if the data returned has a content type of text/xml. (XML documents are discussed in Chapter 4.) So, to get the text contained in info.txt, the call would be as follows:

var sData = oXmlHttp.responseText;

Note that this will return the text in info.txt only if the file was found and no errors occurred. If, for example, info.txt didn't exist, then the responseText would contain the server's 404 message. Fortunately, there is a way to determine if any errors occurred.

The status property contains the HTTP status code sent in the response, and statusText contains the text description of the status (such as "OK" or "Not Found"). Using these two properties, you can make sure the data you've received is actually the data you want or tell the user why the data wasn't retrieved:

Generally, you should always ensure that the status of a response is 200, indicating that the request was completely successful. The readyState property is set to 4 even if a server error occurred, so just checking that is not enough. In this example, the responseText property is shown only if the status is 200; otherwise, the error message is displayed.

The statusText property isn't implemented in Opera and sometimes returns an inaccurate description in other browsers. You should never rely on statusText alone to determine if an error occurred.

As mentioned previously, it's also possible to access the response headers. You can retrieve a specific header value using the getResponseHeader() method and passing in the name of the header that you want to retrieve. One of the most useful response headers is Content-Type, which tells you the type of data being sent:

This code snippet checks the content type of the response and displays an alert indicating the type of data returned. Typically, you will receive only XML data (content type of text/xml) or plain text (content type of text/plain) from the server, because these content types are the easiest to work with using JavaScript.

If you'd prefer to see all headers returned from the server, you can use the getAllResponseHeaders() method, which simply returns a string containing all of the headers. Each heading in the string is separated by either a new line character (\n in JavaScript) or a combination of the carriage return and new line (\r\n in JavaScript), so you can deal with individual headers as follows:

This example splits the header string into an array of headers by using the JavaScript split() method for strings and passing in a regular expression (which matches either a carriage return/new line couple or just a new line). Now you can iterate through the headers and do with them as you please. Keep in mind that each string in aHeaders is in the format headername: headervalue.

It's also possible to set headers on the request before it's sent out. You may want to indicate the content type of data that you'll be sending, or you may just want to send along some extra data that the server may need to deal with the request. To do so, use the setRequestHeader() method before calling send():

In this code, a header named myheader is added to the request before it's sent out. The header will be added to the default headers as myheader: myvalue.

Up to this point, you've been dealing with asynchronous requests, which are preferable in most situations. Sending synchronous requests means that you don't need to assign theonreadystatechange event handler because the response will have been received by the time the send() method returns. This makes it possible to do something like this:

Sending the request synchronously (setting the third argument of open() to false) enables you to start evaluating the response immediately after the call to send(). This can be useful if you want the user interaction to wait for a response or if you're expecting to receive only a very small amount of data (for example, less than 1K). In the case of average or larger amounts of data, it's best to use an asynchronous call.

It's time to revisit the hidden frame GET example to see how the process could be improved using XMLHttp. The first change will be to GetCustomerData.php, which must be changed from an HTML page to simply return an HTML snippet. The entire file now becomes streamlined:

As you can see, there are no visible HTML or JavaScript calls in the page. All the main logic remains the same, but there are two additional lines of PHP code. The first occurs at the beginning, where the header() function is used to set the content type of the page. Even though the page will return an HTML snippet, it's fine to set the content type as text/plain, because it's not a complete HTML page (and therefore wouldn't validate as HTML). You should always set the content type in any page that is sending non-HTML to the browser. The second added line is towards the bottom, where the $sInfo variable is output to the stream by using the echo command.

Note that the function begins the same way, by retrieving the ID the user entered. Then, an XMLHttp object is created using the zXml library. The open() method is called, specifying an asynchronous GET request for GetCustomerData.php (which has the aforementioned ID added to its query string). Next comes the assignment of the event handler, which checks for a readyState of 4 and then checks the status of the request. If the request was successful (status of 200), the displayCustomerInfo() function is called with the response body (accessed via responseText). If there was an error (status is not 200), then the error information is passed to displayCustomerInfo().

There are several differences between this and the hidden frame/iframe example. First, no JavaScript code is required outside of the main page. This is important because any time you need to keep code in two different places there is the possibility of creating incompatibilities; in the frame-based examples, you relied on separate scripts in the display page and the hidden frames to communicate with one another. By changing GetCustomerInfo.php to return just the data you're interested in, you have eliminated potential problems with JavaScript calling between these locations. The second difference is that it's much easier to tell if there was a problem executing the request. In previous examples, there was no mechanism by which you could identify and respond to a server error in the request process. Using XMLHttp, all server errors are revealed to you as a developer, enabling you to pass along meaningful error feedback to the user. In many ways, XMLHttp is a more elegant solution than hidden frames for in-page HTTP requests.

Now that you've seen how XMLHttp can simplify GET requests, it's time to take a look at POST requests. First, you need to make the same changes to SaveCustomer.php as you did for GetCustomerInfo.php, which means you need to remove extraneous HTML and JavaScript, add the content type information, and output the text:

You'll note that the onsubmit event handler has now changed to call the function sendRequest() (although the event handler still returns false to prevent actual form submission). This method first assembles the data for the POST request and then creates the XMLHttp object to send it. The data must be sent in the format as a query string:

name1=value1&name2=value2&name3=value3

Both the name and value of each parameter must be URL-encoded in order to avoid data loss during transmission. JavaScript provides a built-in function called encodeURIComponent() that can be used to perform this encoding. To create this string, you'll need to iterate over the form fields, extracting and encoding the name and value. The getRequestBody() function handles this:

This function assumes that you will supply a reference to the form as an argument. An array (aParams) is created to store each individual name-value pair. Then, the elements of the form are iterated over, building up a string and storing it in sParam, which is then added to the array. Doing this prevents multiple string concatenation, which can lead to slower code execution in some browsers. The last step is to call join() on the array, passing in the ampersand character. This effectively combines all the name-value pairs with ampersands, creating a single string in the correct format.

String concatenation in most browsers is an expensive process because strings are immutable, meaning that once created, they cannot have their values changed. Thus, concatenating two strings involves first allocating a new string and then copying the contents of the two other strings into it. Repeating this process over and over causes a severe slowdown. For this reason, it's always best to keep string concatenations at a minimum and use the array's join() method to handle longer string concatenation.

The sendRequest() function calls getRequestBody() and sets up the request:

As with previous examples, the first step in this function is to get a reference to the form and store it in a variable (oForm). Then, the request body is generated and stored in sBody. Next comes the creation and setup of the XMLHttp object. Note that the first argument of open() is now post instead of get, and the second is set to oForm.action (once again, so this script can be used on multiple pages). You'll also notice that a request header is being set. When a form is posted from the browser to a server, it sets the content type of the request as application/x-www-form-urlencoded. Most server-side languages look for this encoding in order to parse the incoming POST data properly, so it is very important for it to be set.

The onreadystatechange event handler is very similar to that of the GET example; the only change is the call to saveResult() instead of displayCustomerInfo(). The last line is very important, as the sBody string is passed to send() so that it will become part of the request body. This effectively mimics what the browser does, so all server-side logic should work as expected.

Advantages and Disadvantages of XMLHttp

Undoubtedly, you can see the advantage of using XMLHttp for client-server communication instead of hidden frames. The code you write is much cleaner and the intent of the code is much more apparent than using numerous callback functions with hidden frames. You have access to request and response headers as well as HTTP status codes, enabling you to determine if your request was successful.

The downside is that, unlike hidden frames, there is no browser history record of the calls that were made. The Back and Forward buttons do not tie in to XMLHttp requests, so you have effectively cut off their use. It is for this reason that many Ajax applications use a mixture of XMLHttp and hidden frames to make a truly usable interface.

Another disadvantage, which applies to Internet Explorer only, is that you depend on ActiveX controls being enabled. If the user has your page set up in a particular security zone that doesn't allow ActiveX controls, you cannot access the XMLHttp object. In that case, you may have to default to using hidden frames.

This article is adapted from Professional Ajax (Wrox, 2006, ISBN: 0-471-77778-1), from chapter 2 "Ajax Basics."

Copyright 2006 by WROX. All rights reserved. Reproduced here by permission of the publisher.