Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Ajax Patterns And Best Practices (2006)

.pdf
Скачиваний:
39
Добавлен:
17.08.2013
Размер:
15.94 Mб
Скачать

C H A P T E R 2 T H E N U T S A N D B O L T S O F A J A X

29

The Factory pattern is implemented as a single method, FactoryXMLHttpRequest, which returns an XMLHttpRequest object instance. In the implementation of the method are two if statements. The first if statement tests whether the window.XMLHttpRequest object exists. If window.XMLHttpRequest exists, then the object XMLHttpRequest can be instantiated, which most likely includes all browsers except Microsoft Internet Explorer. The second test, window. ActiveXObject, is used if the browser is Internet Explorer. When instantiating the XMLHttpRequest object for Internet Explorer, multiple versions are tested and instantiated. If the instantiation does not work, an exception is generated and caught by the try...catch block. If the if statement does not work or the XMLHttpRequest type could not be instantiated, the function does not return null, but an exception.

It is important to throw an exception so that a developer diagnosing why a script had problems knows where the problem occurred. Many developers would be inclined to return a null value, but that is an incorrect response. When a script calls the FactoryXMLHttpRequest method,

it is expected to return an instance of XMLHttpRequest. If an instance cannot be returned, it is an error and an exception must be thrown.

Rewriting the Ajax Application to Use a Factory

In this section, the minimal Ajax application shown previously is rewritten to use the FactoryXMLHttpRequest method so that all browsers can run the Ajax application. Following is the rewritten HTML page:

<html><head>

<title>Sample Page</title> </head>

<script language="JavaScript" src="/lib/factory.js"></script> <script language="JavaScript" type="text/javascript">

var xmlhttp = FactoryXMLHttpRequest();

function GetIt(url) { if( xmlhttp) {

xmlhttp.open('GET', url, false); xmlhttp.send(null);

document.getElementById('result').innerHTML = xmlhttp.responseText;

}

}

</script>

</head>

<body>

<button onclick="GetIt('/cgross/books')">Get a document</button> <p><table border="1">

<tr><td>Document</td><td><span id="result">No Result</span></td></tr> </table></p>

</body>

</html>

30

C H A P T E R 2 T H E N U T S A N D B O L T S O F A J A X

The rewritten page loads the XMLHttpRequest Factory pattern implementation by using a script tag, and assigning the attribute src to be the name of the file containing the Factory pattern implementation. Then, to instantiate and assign the XMLHttpRequest instance to the variable xmlhttp, the function FactoryXMLHttpRequest is called. The remaining code remains identical to the previous example because regardless of the browser, the methods of

XMLHttpRequest are identical.

Making Asynchronous Requests

The Ajax examples used the XMLHttpRequest object in a synchronous manner, meaning that the moment send is called, the browser stops processing other messages and waits for an answer. To illustrate that a browser locks while processing synchronous requests, the previous Ajax application will retrieve a page from a server that will wait 10 seconds before returning the content. Following is the ASP.NET source code (note that this book will focus on both Java and ASP.NET):

<%@ Page Language = "C#" %> <html>

<head>

<title>Hanging page</title> </head>

<body>

<%

System.Threading.Thread.Sleep( 10000); %>

Hello, after a ten second sleep! </body>

</html>

The ASP.NET sample is written by using the C# programming language. The single statement, System.Threading.Thread.Sleep, causes the current thread on the server to sleep for 10 seconds, which means that the browser will be waiting 10 seconds for its content to be retrieved.

Modifying the previous Ajax application and clicking the button to retrieve the hanging page causes the browser to appear similar to Figure 2-5.

In Figure 2-5, the clicked button remains pressed because it is waiting for the content to be returned. While the browser is waiting, the user cannot switch to another tab to process other HTTP requests. A hanging browser is a problem and will make the Ajax experience potentially painful for the user.

C H A P T E R 2 T H E N U T S A N D B O L T S O F A J A X

31

Figure 2-5. Hanging browser waiting for content to be retrieved

The solution is to use an asynchronous Ajax XMLHttpRequest request. An asynchronous request will not block the browser, and the user could continue clicking or using other tabs of the browser. The following source code rewrites the simple Ajax application to use an asynchronous request:

<html>

<head>

<title>Sample Page</title> </head>

<script language="JavaScript" src="/lib/factory.js"></script> <script language="JavaScript" type="text/javascript">

var xmlhttp = FactoryXMLHttpRequest();

function AsyncUpdateEvent() { switch(xmlhttp.readyState) { case 0:

document.getElementById('status').innerHTML = "uninitialized"; break;

case 1:

document.getElementById('status').innerHTML = "loading"; break;

case 2:

document.getElementById('status').innerHTML = "loaded"; break;

32 C H A P T E R 2 T H E N U T S A N D B O L T S O F A J A X

case 3:

document.getElementById('status').innerHTML = "interactive"; break;

case 4:

document.getElementById('status').innerHTML = "complete"; document.getElementById('result').innerHTML = xmlhttp.responseText; break;

}

}

function GetIt(url) { if(xmlhttp) {

xmlhttp.open('GET', url, true); xmlhttp.onreadystatechange = AsyncUpdateEvent; xmlhttp.send(null);

}

}

</script>

</head>

<body>

<button onclick="GetIt('/chap02/serverhang.aspx')">Get a document</button> <p><table border="1">

<tr>

<td>Document</td>

<td>

<span id="status">No Result</span> </td>

<td>

<span id="result">No Result</span> </td></tr>

</table></p>

</body>

</html>

There are several new additions to the rewritten Ajax application, and they deal with the technical issues of loading content asynchronously. Let’s start by focusing on the function GetIt. The implementation of GetIt is similar to previous Ajax application examples, except that the third parameter of the method open is true to indicate that the request will be asynchronous. This means that when the method send is called, the method will send the request, start another thread to wait for the response, and return immediately.

Whenever XMLHttpRequest operates in asynchronous modes, feedback is given to the caller on the state of the request. The property onreadystatechange is a function that receives the feedback. It is important to note that the feedback function must be assigned before each send because upon completion of the request, the property onreadystatechange is reset. This is evident in the sources of the Mozilla-based browsers.

The property onreadystatechange is assigned the function AsyncUpdateEvent. In the implementation of AsyncUpdateEvent is a switch statement that tests the current state of the request. When an asynchronous request is made, the script is free to continue executing other code.

C H A P T E R 2 T H E N U T S A N D B O L T S O F A J A X

33

This could cause problems if the script attempts to read the request results before the request has been completed. Using the property readyState, it is possible to know the stage of the HTTP request. The property readyState can contain one of five values, each representing a request state:

0: The XMLHttpRequest instance is in an inconsistent state, and the result data should not be referencing.

1: A request is in progress, and the result data should not be retrieved.

2: The request has downloaded the result data and is preparing it for reference.

3: The script can interact with the XMLHttpRequest instance even though the data is not completely loaded.

4: The request and result data are completely downloaded and loaded as an object model.

The request states seem to indicate that it is possible to manipulate various properties at different states. The problem is that not all browsers support the same property states at the same state codes. The only cross-platform solution is to reference the XMLHttpRequest result properties (status, statusText, responseText, and responseXML) when the request state is equal to 4. When the request state is 4, you can be sure that the result properties contain a valid value.

Executing the asynchronous Ajax application results in a call being made, and the browser is not locked. You can click the button, open a new browser, and surf to another website. After the 10 seconds have expired, the generated HTML page should resemble Figure 2-6.

Figure 2-6. Resulting HTML page using asynchronous XMLHttpRequest

The asynchronous approach solves the problem of the hanging browser. The Ajax application could continue processing other data, and in fact multiple requests could be made.

What is not optimal is that there is no busy indicator. You have no idea whether anything is working when you click the OK button. There should be some form of indicator that something is happening.

34

C H A P T E R 2 T H E N U T S A N D B O L T S O F A J A X

Another problem is that some browsers will cache the results of the XMLHttpRequest. This is an age-old problem because caching can result in unpredictable behavior, and caching still happens even if the Ajax HTML page is reloaded.

Making Practical Use of XMLHttpRequest

The Factory pattern implementation that was used to abstract the instantiation of XMLHttpRequest was a good first step. Using an asynchronous request is a good second step, as it improves the Ajax experience, but other problems remain, such as user feedback and how to use security that falls in the context of same origin policy.

Implementing an Asynchronous Calling Mechanism

When executed in the context of a web browser, JavaScript is not a multithreaded programming language, and therefore it is not possible to instantiate a thread that processes some data, while the main Ajax application is executing. Using an asynchronous XMLHttpRequest instance is sort of multithreading in that the application can continue execution while waiting for a response. Asynchronous programming means writing event-driven code, and that requires a different way of programming with JavaScript. Yet writing code with JavaScript is not like writing code in an object-oriented language. JavaScript is more or less a procedural language that has some hand-wired extensions that make it appear object oriented.

The Modified Ajax Application

In this section, I’m going to again modify the Ajax application that has been illustrated multiple times, except this time I’ll add a button to make another request. To illustrate asynchronous programming, two requests will be made simultaneously. One request will return immediately with the data, and the second will call the 10-second delay page. Following is the modified HTML code:

<html>

<head>

<title>Sample Page</title> </head>

<script language="JavaScript" src="/lib/factory.js"></script> <script language="JavaScript" src="/lib/asynchronous.js"></script> <script language="JavaScript" type="text/javascript">

function AsyncUpdateEvent(status, statusText, responseText, responseXML) { document.getElementById('httpcode').innerHTML = status; document.getElementById('httpstatus').innerHTML = statusText; document.getElementById('result').innerHTML = responseText; document.getElementById('xmlresult').innerHTML = responseXML;

}

C H A P T E R 2 T H E N U T S A N D B O L T S O F A J A X

35

function AsyncUpdateEvent2(status, statusText, responseText, responseXML) { document.getElementById('httpcode2').innerHTML = status; document.getElementById('httpstatus2').innerHTML = statusText; document.getElementById('result2').innerHTML = responseText; document.getElementById('xmlresult2').innerHTML = responseXML;

}

var asynchronous = new Asynchronous(); asynchronous.complete = AsyncUpdateEvent; var asynchronous2 = new Asynchronous(); asynchronous2.complete = AsyncUpdateEvent2;

</script>

</head>

<body>

<button onclick="asynchronous.call('/chap02/serverhang.aspx')"> Get a document</button>

<button onclick="asynchronous2.call('/books/cgross')"> Get a document2</button>

<p><table border="1"> <tr><td>Document</td>

<td><span id="httpcode">No Http Code</span></td> <td><span id="httpstatus">No Http Status</span></td> <td><span id="result">No Result</span></td>

<td><span id="xmlresult">No XML Result</span></td></tr> <tr><td>Document</td>

<td><span id="httpcode2">No Http Code</span></td> <td><span id="httpstatus2">No Http Status</span></td> <td><span id="result2">No Result</span></td>

<td><span id="xmlresult2">No XML Result</span></td></tr> </table></p>

</body>

</html>

Going through the HTML code from the top to the bottom, near the top of the HTML code are three script tags. The first two reference the files factory.js and asynchronous.js. The file factory.js contains the XMLHttpRequest factory used for instantiation purposes. The file asynchronous.js is new and it contains the code to make asynchronous HTTP requests. For the moment, ignore the exact details of this file and just assume it is a black box that works. The last script tag contains the JavaScript code to update the HTML page.

In the JavaScript code are two functions: AsyncUpdateEvent and AsyncUpdateEvent2, which are similar but not identical. Each of the functions updates one of rows of the HTML table and is wired to be called when the HTTP request completes.

36

C H A P T E R 2 T H E N U T S A N D B O L T S O F A J A X

In the middle of the HTML code, near the end of the last script tag, is the instantiation of the variables asynchronous and asynchronous2. Each of these variables is of the type Asynchronous, which is a class that encapsulates the XMLHttpRequest asynchronous functionality. When the buttons call Asynchronous.call, an HTTP GET request is made. When the request completes, the Asynchronous class calls the functions AsyncUpdateEvent and AsyncUpdateEvent2 with the retrieved data. The Asynchronous class calls the functions because in the JavaScript code the functions are wired to Asynchronous via the property complete. In the example HTML code, instantiating two instances allows two simultaneous HTTP requests.

The Asynchronous Class

The Asynchronous class is a JavaScript class that encapsulates the XMLHttpRequest functionality. The user of a class is expected to assign specific properties to receive feedback on the status of a request. In the modified Ajax application, the property complete was assigned to the functions AsyncUpdateEvent and AsyncUpdateEvent2 to process the request’s returned data.

Following is the implementation of the asynchronous.js file:

function Asynchronous( ) {

this._xmlhttp = new FactoryXMLHttpRequest();

}

function Asynchronous_call(url) { var instance = this;

this._xmlhttp.open('GET', url, true); this._xmlhttp.onreadystatechange = function() {

switch(instance._xmlhttp.readyState) { case 1:

instance.loading();

break; case 2:

instance.loaded();

break; case 3:

instance.interactive();

break; case 4:

instance.complete(instance._xmlhttp.status, instance._xmlhttp.statusText,

instance._xmlhttp.responseText, instance._xmlhttp.responseXML); break;

}

}

this._xmlhttp.send(null);

}

C H A P T E R 2 T H E N U T S A N D B O L T S O F A J A X

37

function Asynchronous_loading() {

}

function Asynchronous_loaded() {

}

function Asynchronous_interactive() {

}

function Asynchronous_complete(status, statusText, responseText, responseHTML) {

}

Asynchronous.prototype.loading = Asynchronous_loading;

Asynchronous.prototype.loaded = Asynchronous_loaded;

Asynchronous.prototype.interactive = Asynchronous_interactive;

Asynchronous.prototype.complete = Asynchronous_complete;

Asynchronous.prototype.call = Asynchronous_call;

To declare a class in JavaScript, you need to declare a function with the name of the class. The declared function is called a constructor. In the case of the class Asynchronous, you would declare a function with the identifier Asynchronous. When a class is instantiated by using the new keyword, the object instance is empty, or more simply put, it has no methods or properties.

You can define default properties and methods by using the prototype property. When using the prototype property, each defined method and property is shared by all instances of the type. For the class Asynchronous, there are four shared methods. The methods—loading, loaded, interactive, and complete, are called whenever the asynchronous request updates its status. For the default case, all the status methods do nothing and are placeholders so that no exceptions are generated. If the prototype property were not used and the methods were assigned in the constructor, each instance would have its own copy of a function.

When the Asynchronous class is instantiated, an object with five methods is created. To be able to reference the data of the object instance, the this keyword must be used. In the Asynchronous constructor, the data member _xmlhttp is assigned an instance of XMLHttpRequest by using the factory function FactoryXMLHttpRequest. This means that for every instantiated Asynchronous class, an instance of XMLHttpRequest is associated.

Cross-referencing the Asynchronous class with the HTML code, the class method complete is assigned to reference the methods AsyncUpdateEvent and AsyncUpdateEvent2. When an asynchronous request is finished, the property method complete is called, and it calls the functions AsyncUpdateEvent and AsyncUpdateEvent2. The client script uses the method call to execute an asynchronous request.

The Problem of Multiple Requests and Multiple Callbacks

Before I discuss the function Asychronous_call, I need to explain the problem that Asynchronous_call solves. In the previous section, assigning the property onreadystatechange a function makes it possible to know when the result data is available. For the initial asynchronous XMLHttpRequest request example, the property onreadystatechange was assigned a global function. Now imagine that you want to create multiple requests. That would encompass creating multiple instances of XMLHttpRequest, where each instance was assigned its own function. A more efficient approach would be to use object-oriented principles and the this keyword.

38

C H A P T E R 2 T H E N U T S A N D B O L T S O F A J A X

Consider the following source code that seems correct, but will work incorrectly:

function AsyncUpdateEvent() {

window.alert( "Who's calling (" + this.myState + ")");

}

function GetIt(xmlhttp, url) { if( xmlhttp) {

xmlhttp.open('GET', url, true); xmlhttp.onreadystatechange = AsyncUpdateEvent; xmlhttp.send(null);

}

}

var xmlhttp1 = FactoryXMLHttpRequest(); xmlhttp1.myState = "xmlhttp1";

var xmlhttp2 = FactoryXMLHttpRequest(); xmlhttp2.myState = "xmlhttp2";

GetIt(xmlhttp1, '/chap02/serverhang.aspx');

GetIt(xmlhttp2, '/books/cgross');

The functions GetIt and AsyncUpdateEvent are like previous examples in which asynchronous function calls were made. New to the function GetIt is the additional parameter xmlhttp. This was added so that multiple XMLHttpRequest instances could be used with GetIt. The variables xmlhttp1 and xmlhttp2 represent two different instances of XMLHttpRequest, and assigned to each instance is the data member myState. To make two separate HTTP requests, GetIt is called twice with different XMLHttpRequest instances and different URLs.

When the asynchronous XMLHttpRequest returns, the function AsyncUpdateEvent is called. The function AsyncUpdateEvent is assigned to the instance of either xmlhttp1 or xmlhttp2, and therefore in the implementation of the function, the this keyword should work. What happens is that the this.myState reference in the function is undefined, and therefore AsyncUpdateEvent has no idea to which XMLHttpRequest instance it is assigned.

A solution would be to create two callback functions, AsyncUpdateEvent and AsyncUpdateEvent2, and assign them individually to the instances xmlhttp1 and xmlhttp2. The function GetIt would be updated to include an additional parameter that represents the callback where the request results are processed. Creating two callback functions would work but is not elegant because for three independent requests you would need three callbacks. The real context of this problem is that JavaScript in this instance has lost its object-oriented features. What needs to be solved is the association of an XMLHttpRequest instance with a callback, and that is solved in the next section.