本文引用自 |
by Matthew Eernisse
05/19/2005
I began working with web applications back in the bad old days, when making an application behave like a desktop app meant wrestling with byzantine table-based layouts nested five and six levels deep, and horrid, hackish frame sets within frame sets within frame sets. Those were the days.
Things have steadily improved for web developers with the advent of standards-compliant browsers, CSS, DHTML, and the DOM. Pervasive broadband access has made web apps feel a lot snappier. Now something called the XMLHttpRequest
object makes it even easier to develop full-blown, superinteractive applications to deploy in the browser.
While not exactly new, the XMLHttpRequest
object is receiving more attention lately as the linchpin in a new approach to web app development, most recently dubbed Ajax (asynchronous JavaScript and XML), which powers the cool features found on sites like Flickr, Amazon's A9.com, and the new poster children for whizzy web-based interactivity, Google Maps and Google Suggest. The snazzy Ajax moniker seems to be getting some momentum--it's popping up in all sorts of places, including the Ajaxian weblog and the recent Ajax Summit put together by O'Reilly Media and Adaptive Path.
Cool acronym or not, when I decided a while back to add a long overdue Search Playlist feature to my webcast radio station, EpiphanyRadio, it seemed like a good opportunity to show off some of the features the XMLHttpRequest
object offers. The Search feature accesses a PostgreSQL database of the tracks in current rotation and allows listeners to search by artist, song title, and other criteria.
As it turns out, it's pretty easy to take advantage of the XMLHttpRequest
object to make a web app act more like a desktop app--while still using traditional tools like web forms for collecting user input. I also found some great ways to handle server-side errors to make debugging less of a headache.
Introducing the Object
The XMLHttpRequest
object allows client-side JavaScript to make HTTP requests (both GET
and POST
) to the server without reloading pages in the browser or resorting to iframe
tricks or other ad hockery. Microsoft implemented the XMLHttpRequest
object in Internet Explorer on Windows as an ActiveX object beginning with version 5. The Mozilla project added it in Mozilla 1.0 as a native object with a compatible API. Apple added it to Safari in version 1.2.
Note that despite its name, you can use the XMLHttpRequest
object with more than just XML. You can use it to request or send any kind of data--keep in mind, though, that JavaScript processes the response from the server. The following example returns the data to the browser in a simple DSV (delimiter-separated values) format.
Preparing Form Data to POST
Other resources online covering the XMLHttpRequest
object demonstrate how to use it with simple GET
requests. The object is also capable of doing POST
s, which makes it a much more useful tool for creating web applications.
To use the POST
method, pass data to the XMLHttpRequest
object in query-string format (for example, ArtistName=Kolida&SongName=Wishes
), which the object then sends to the server just like a normal form POST
. You can use JavaScript to pull data a piece at a time out of web-form elements, and then format the data into a query string. If I wanted to do this for my playlist search function, I could use something like this:
var searchForm = document.forms['searchForm'];
var artistName = searchForm.ArtistName.value;
var songName = searchForm.SongName.value;
var queryString = '?ArtistName=' + escape(artistName) + '&SongName=' +
escape(songName);
Note: Be sure to escape
(URL encode) the values.
Of course, keeping in mind that one of Larry Wall's three great virtues of a programmer is laziness (and in that respect I'm really virtuous), I wrote a general function to automate this process, to put all the data from a form into a query string. This function allows me to POST
form data with the XMLHttpRequest
object without having to do a lot of extra work every time. That makes it significantly easier to integrate the use of the object with my existing application code.
Here's a quick look at the first part of the function:
function formData2QueryString(docForm) {
var strSubmit = '';
var formElem;
var strLastElemName = '';
for (i = 0; i < docForm.elements.length; i++) {
formElem = docForm.elements[i];
switch (formElem.type) {
// Text, select, hidden, password, textarea elements
case 'text':
case 'select-one':
case 'hidden':
case 'password':
case 'textarea':
strSubmit += formElem.name +
'=' + escape(formElem.value) + '&'
break;
The variable docForm
is a reference to the form from which to pull the data. This lets me reuse the function in other places.
The function iterates through the form's elements
collection, using the type
of each element to figure out how to retrieve its value. For each distinctly named element, it appends the name and value onto the query string variable strSubmit
. It then hands off that string to the XMLHttpRequest
object for the POST
.
For most types of form elements, looking up its value
property does the trick. However, radio buttons and check boxes require a bit more work. For check box sets, I create a comma-delimited string for the value, but you can handle them in whatever way suits you best. This function is a huge time-saver when working with the XMLHttpRequest
object and collecting user data from a form.
The entire function is available for download if you'd like to try it.
Creating the Object
To create the object in JavaScript for IE, use new ActiveXObject("Microsoft.XMLHTTP")
. In Mozilla/Firefox and Safari, use new XMLHttpRequest()
. The first half of a simple function to create and use the object looks like this:
function xmlhttpPost(strURL, strSubmit, strResultFunc) {
var xmlHttpReq = false;
// Mozilla/Safari
if (window.XMLHttpRequest) {
xmlHttpReq = new XMLHttpRequest();
xmlHttpReq.overrideMimeType('text/xml');
}
// IE
else if (window.ActiveXObject) {
xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");
}
To make this a reusable, generic function, I pass in three parameters: the URL for the processing page on the server, the query-string formatted data to submit, and the name of the JavaScript function that will process the response from the server (to invoke later through eval
).
Note the addition of the overrideMimeType
method call in the Mozilla/Safari code. Without this, some people have reported that some versions of Mozilla lock up when the server returns anything other than XML. (I cannot confirm this issue, as I have not experienced this problem myself.)
Also, if you want to support older browsers, you can then test the xmlHttpReq
variable and fall back to other methods of submitting data if the object is not present.
POST
ing the Data
Here's the rest of the function, which submits the request to the server:
xmlHttpReq.open('POST', strURL, true);
xmlHttpReq.setRequestHeader('Content-Type',
'application/x-www-form-urlencoded');
xmlHttpReq.onreadystatechange = function() {
if (xmlHttpReq.readyState == 4) {
eval(strResultFunc + '(xmlHttpReq.responseText;);');
}
}
xmlHttpReq.send(strSubmit);
}
The open
method here takes three parameters: the first sets the request method, the second is the processing page, and the third sets the async
flag, governing whether the function continues executing immediately after sending the request or waits for a reply before continuing.
Note: According to the HTTP 1.1 spec, the request method is case-sensitive. It doesn't seem to matter using Internet Explorer, but in Mozilla if you enter the request method in lowercase, the request will fail.
The onreadystatechange
property sets a callback function (for example, xmlHttpReq.onreadystatechange = handleResponse;
) to execute when the readyState
property changes. When readyState
changes to a value of 4, the request has completed. The code above uses an anonymous function (instead of passing off the result to a separate function) that watches until the response comes back and passes the result as a string to the processing function.
Finally, the send
method actually sends the request. It takes one parameter: the data to submit to the server. In the function here, this is strSubmit
, the query string created from the form data with the formData2QueryString
function earlier.
The Server Response
I mentioned earlier that despite its name, the XMLHttpRequest
object works with other types of data besides XML. This makes me happy, because the search function for my internet radio station returns very simple tabular data of track listings. Returning results as DSV instead of XML significantly reduces the size and complexity of the return data and simplifies parsing. (As Eric S. Raymond notes in the Data File Metaformats chapter of The Art of Unix Programming, "XML is well suited for complex data formats ... though overkill for simpler ones.") You can use whatever separator you want, but for this I used a pipe (|
) character.
The back-end code for the Search Playlist feature is Ruby code running under mod_ruby, as is most of the site. Ruby may not be as familiar to developers as PHP or Perl, but its flexibility, extensibility, and clean syntax make it an ideal web development platform.
nRowCount = sth.size
strContent += nRowCount.to_s + 10.chr + 10.chr
sth.each do |row|
strContent += row['artist'] + '|' + row['song'] + '|' +
row['album'] + '|' + row['comment'] + 10.chr
end
In this example, sth
is an array containing the result of the database query performed with the Ruby DBI module. The sth.each do |row|
line may look a bit odd to the non-Rubyist, but it's an example of an iterator/block combination, one of the many interesting and powerful features Ruby offers. In this case, as you may have guessed, it's pretty much equivalent to a foreach
in other languages.
The 10.chr
is a linefeed character. Essentially this chunk of code writes out the row count followed by two linefeeds, then writes out each returned row on a single line with the fields separated by pipe characters. A sample search result looks like this:
4
Kush|New Life With Electricity|The Temptation Sessions||
Kush|Plaster Paris (Part Two)|The Temptation Sessions||
Kush|Reverse (Part One)|The Temptation Sessions||
Kush|The Beauty of Machines at Work|The Temptation Sessions||
The two pipe characters together at the end indicate a blank field for the comment
column.
Processing the Response
When the response comes back from the server, the XMLHttpRequest
object can access it through two properties: responseXML
(as an XML document) and responseText
(as a string). Because I eschewed the unnecessary complexity of XML here, the code passes the responseText
off to a JavaScript function for the processing and display of the returned data with this line from the original function:
eval(strResultFunc + '(xmlHttpReq.responseText;);');
This takes the function name passed into the xmlhttpPost
function and executes it using eval
, passing the XMLHttpRequest
object's responseText
as a parameter.
After the code has split the string into an array, you have several ways to use it to populate a table for display. Not being a huge fan of the DOM table modification methods (like XML, they're just too verbose for my tastes), I generally take the straightforward approach of using innerHTML
. Here's a sketch of the JavaScript I use to process the result for my playlist search:
function displayResult(strIn) {
var strContent = '<table>';
var strPrompt = '';
var nRowCount = 0;
var strResponseArray;
var strContentArray;
var objTrack;
// Split row count / main results
strResponseArray = strIn.split('/n/n');
// Get row count, set prompt text
nRowCount = strResponseArray[0];
strPrompt = nRowCount + ' row(s) returned.';
// Actual records are in second array item --
// Split them into the array of DB rows
strContentArray = strResponseArray[1].split('/n');
// Create table rows
for (var i = 0; i < strContentArray.length-1; i++) {
// Create track object for each row
objTrack = new trackListing(strContentArray[i]);
// ----------
// Add code here to create rows --
// with objTrack.arist, objTrack.title, etc.
// ----------
}
strContent += '</table>';
// ----------
// Use innerHTML to display the prompt with rowcount and results
// ----------
}
The Ruby code on the server separated the row count from the actual data rows by two linefeed characters; this function thus pulls out the count by splitting the entire results string on two linefeeds and using the first item in the resulting array.
The actual data rows are in the second item in that array. The data rows have single linefeeds separating them, so another split on a single linefeed of that item creates the main array of the data to write to the page. To create the table content, the code iterates over that array, creating a trackListing
object for each row and using that object to make an HTML table row. The trackListing
function creates trackListing
objects:
function trackListing(strEntry) {
var strEntryArray = strEntry.split('|');
this.artist = strEntryArray[0];
this.title = strEntryArray[1];
this.album = strEntryArray[2];
this.label = strEntryArray[3];
}
It splits the pipe-delimited string for each row and sets named properties for each object matching the column names in the database. You could omit this bit and instead use numbered array items for each column back in the main function, but I think it's easier to refer to them with names.
Handling Errors
The XMLHttpRequest
object has a huge upside: it allows JavaScript to communicate directly with the server without loading a page in the browser. Unfortunately, however, that also becomes its downside when the inevitable Bad Things happen on the back end. If you usually work with languages that can return errors directly in the browser window, it can feel like flying blind when you're trying to debug a page that uses the XMLHttpRequest
object--especially in environments where you don't have easy access to server error logs.
The object does have a status
property, which contains the numeric code returned by the server (for example, 404, 500, and 200), and there is an accompanying statusText
property, which is a brief string message. In the case of a server-side (code 500) error, this message merely states the obvious "Internal Server Error," which might be OK to display to the user but is pretty worthless for debugging.
The normal 500 error page returned from the server quite often contains extremely helpful debug information such as the error type, the line number on which the error occurred, and even a full backtrace of the error. Unfortunately, with the XMLHttpRequest
object, all that stuff ends up buried in a JavaScript string variable.
It's actually fairly simple to retrieve the full-page 500 error messages and to accommodate debugging in a more elegant way. To accomplish this I had to add code to the original function for creating and using the XMLHttpRequest
object:
if (xmlHttpReq.readyState == 4) {
strResponse = xmlHttpReq.responseText;
switch (xmlHttpReq.status) {
// Page-not-found error
case 404:
alert('Error: Not Found. The requested URL ' +
strURL + ' could not be found.');
break;
// Display results in a full window for server-side errors
case 500:
handleErrFullPage(strResponse);
break;
default:
// Call JS alert for custom error or debug messages
if (strResponse.indexOf('Error:') > -1 ||
strResponse.indexOf('Debug:') > -1) {
alert(strResponse);
}
// Call the desired result function
else {
eval(strResultFunc + '(strResponse);');
}
break;
}
}
The case
statement handles the response from the server differently depending on the xmlHttpReq.status
value. It hands off the response to an error-handling function in the case of a full-blown error, but it still allows for a simple JavaScript alert
to display a simple error message to the user (perhaps, "Error: e-mail address does not match the one for this account."), or to print a little debug message for the developer. (You could also print out those types of errors in nicely formatted text to a div
somewhere on the page.)
Here's the function that creates the full-screen 500 error page:
function handleErrFullPage(strIn) {
var errorWin;
// Create new window and display error
try {
errorWin = window.open('', 'errorWin');
errorWin.document.body.innerHTML = strIn;
}
// If pop-up gets blocked, inform user
catch(e) {
alert('An error occurred, but the error message cannot be' +
' displayed because of your browser/'s pop-up blocker./n' +
'Please allow pop-ups from this Web site.');
}
}
The try
/catch
is important because of the ubiquity of pop-up blockers. If the user has blocked pop-ups, at least she has the option of allowing them so that she can see and report the error properly.
If the user allows pop-ups, a server-side 500 error will produce a new window containing the all-too-familiar page displaying all that helpful info you need to track down and squash the bug. (You might decide you'd rather display the error message in the current window, but my playlist search is a small pop-up window that will not display the entire message properly. Most standard-issue 500 error pages assume a full-size window.)
Notes
The XMLHttpRequest
object transmits cookies with its requests, just like normal requests from the browser, so you don't have to do any special gymnastics to make server-side sessions work.
I haven't had much luck using the same object for multiple requests, particularly when invoking it from multiple windows. Use a new XMLHttpRequest
object for each request. That will also save you trouble when doing multiple, asynchronous requests.
Requests made with the XMLHttpRequest
object do not affect the browser history. That can cause serious user confusion, because clicking on the browser's Back button may not change things back to their previous state. In cases where you need history steps to correspond to user actions, you're better off using an iframe
to make HTTP requests, because it creates history entries for each request.
Conclusion and Future Plans
The XMLHttpRequest
object provides some serious kung fu to help web developers make web applications more responsive and dynamic--and make them perform more like desktop apps. With a little setup beforehand, such as the formData2QueryString
and handleErrFullPage
functions to handle the quirks of working with the object, you can get in on the Ajax action without having to alter your development process significantly.
Once you've started down the Ajax path, of course, you begin to see other places in your app where it would be a great fit. I can already see using it with the Previously Played feature on EpiphanyRadio. It would be a snap to have the XMLHttpRequest
object poll the Song History page on the SHOUTcast server periodically. I could then have JavaScript pull the song list out of the HTML page response and write it out with DHTML to a div
. The list of previously played tracks would continuously update without ever having to reload the page in the browser.
I could use it on the listener log-in page, to display nicely formatted log-in error messages on the original log-in screen instead of redirecting to a completely different page. I could use it to provide a continuous update of the number of current listeners by polling the main SHOUTcast site and pulling the numbers out of the response using a smart regular expression.
Other References
- Dynamic HTML and XML: The XMLHttpRequest Object
- Using the XML HTTP Request object
- XMLHttpRequest
- Using XMLHTTPRequest in Mozilla
- IE7 XML Extras (This is a third-party JavaScript add-on for IE 6.)
Matthew Eernisse is the lead web application developer for an enterprise-class learning management system at an e-learning solutions company.
Return to ONLamp.com.
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 11 of 11.
- select-one in formData2QueryString
2005-11-16 08:09:05 inthe80s [Reply | View]Reply | View]
While the code provided will work fine in a decent browser like Firefox, Internet Explorer still requires you to pull the index and refer to the text and not just pull the value of the form element. So remove case 'select-one' from the switch, and add it back as a seperate one here:
case 'select-one':
strSubmitContent += formElem.name + '=' + escape(formElem[formElem.selectedIndex].text) + '&'
break;
- Two bugs
2005-11-10 04:37:14 ilyaporopudas [Reply | View]Reply | View]
- Under the heading "Preparing form data to POST", on the line:
var queryString = '?ArtistName=' + escape(artistName) + '&SongName=' +
escape(songName);
The value of "queryString" variable should not start with a question mark. - Under the heading "POSTing the data", on the line of code:
eval(strResultFunc + '(xmlHttpReq.responseText;);')
There's one semi-colon too many. The one after "xmlHttpReq.responseText" should be removed.
- Under the heading "Preparing form data to POST", on the line:
- Similar Article , Using Struts instead of Ruby
2005-10-30 06:16:22 paulbrowne [Reply | View]Reply | View]
Good article, explains everything very clearly , and has made me want to learn more about Ruby!
A similar Article, using Java Struts instead of Ruby on the Server side is published on OnJava's sister site (Java.net) - link follows:
http://today.java.net/pub/a/today/2005/10/27/sprinkle-ajax-magic-into-struts-webapp.html
- File Upload Problem
2005-10-26 23:11:15 asif_royal [Reply | View]Reply | View]
when i m trying to post the file path using input type file in html via AJAX in variable filename to php page.But onphp page while uploading file using syntax of php $_FILE['filename'] does create any output. plz try to resolve this problem
- Using overrideMimeType with text files causes errors
2005-10-09 02:57:41 parsingphase [Reply | View]Reply | View]
Using http_request.overrideMimeType('text/xml'); when you're not actually fetching XML is incorrect, and will cause confusing Javascript Console errors in the latest versions of Firefox. The XMLHttpRequest object will try and parse the fetched file as XML, and complain when it can't.
See:
https://bugzilla.mozilla.org/show_bug.cgi?id=311724
(Mozilla bug 311724)
- Only uses the first form on a page
2005-06-15 05:45:12 maYO [Reply | View]Reply | View]
In your example the function formData2QueryString will always and only take the contents from the very first form on a page.
Changing two lines in that function would make it more modular:
function formData2QueryString(formName) {
var docForm = document.forms[formName];
.
.
.
So the only thing left to do is call that function and passing it the name of the form as argument.- Only uses the first form on a page
2005-06-15 20:54:11 eernisse [Reply | View]Reply | View]
Oops, sorry -- looks like the downloadable version has a slightly older version of the function. The version used as the example in the article is more up-to-date one where the form reference is passed into the function as a variable.
That lets you use the function with forms in other frames or windows, like this:var openerForm = opener.document.forms['searchForm'];
var strPostData = formData2QueryString(openerForm);
I'll update the code in the downloadable file. Thanks for catching that.
- Only uses the first form on a page
- The Case "file"?
2005-06-12 00:19:02 JohnKuang [Reply | View]Reply | View]
Can you tell me how to deal with the case "file"?- The Case "file"?
2005-06-12 11:10:13 eernisse [Reply | View]Reply | View]
Not sure exactly what you mean. Could you clarify your question a bit?- The Case "file"?
2005-08-09 07:10:16 dephaz [Reply | View]Reply | View]
I think JohnKuang wants to talk about the case of an input file in the formData2QueryString function ?
- The Case "file"?
- The Case "file"?
- XMLHttpRequest debugging
2005-06-01 20:57:01 dumky [Reply | View]Reply | View]
For those interested, I just posted a Greasemonkey user script to help debugging XMLHttpRequest in Firefox.
You can get the script and more info on my XMLHttpRequest debugging with Greasemonkey post.