大约五年前我曾参与一个web应用的开发,该应用的一个主要需求是要提供类似window胖客户端的外观和操作方式。先不讨论为什么当初这个项目不直接使用window胖客户端,而把这个难题带到了web开发中,事实是在五年前还没有多少这样的东西(基于web的胖客户端)存在。
作为对这一课题研究的结果,我偶然发现了一些用于实现上述需求的非典型技术和方法。使用这些技术实现的web应用,直到现在很多人还不能相信它们是基于web的,然后事实上你确实是通过浏览器来访问它们。
让我没想到的是,几年后的今天我所实现的那种东西又出现另一种实现方式,它就是AJAX。AJAX是Adaptive Path的人们发明的一个名词,全称是Asynchronous Javascript + XML。
这说明了一件事情,提炼你曾经拥有的好主意是致富的一个好方法。如果当初我意识到我所作的是一件很特别的东西......我跑题了
Google正在使用这项技术,许多其它的组织也是。但它究竟是个什么东西呢?概括地说,AJAX不是一项技术,只是一种考虑问题的方法,这个方法整合了多种技术且基于这样一种考虑:为每一个客户请求构造一张全新的web页面是低效的且应该避免的。
举个例子,假设你再一张web页面上放了两个SELECT元素,你想让第二个SELECT元素的内容随着第一个的内容变化而变化,这是实际开发中很常见的一个问题而且有多种解决方案。
AJAX对此问题的解决方法是:只重画页面的一小部分,在这里是第二个SELECT。
AJAX基于一种称为XMLHttpRequest的组件。讨厌Microsoft的人要开始叫嚷了,因为这是Microsoft的东西。不错, Microsoft有些东西做得挺好,而且先于其他人做了。Microsoft最初实现XMLHttpRequest是在Windows下的IE 5中,其实现方式是ActiveX对象(好吧,他们做得不完全对!)。Monilla项目在Mozilla 1.0中实现了一个本地版本,还有Netscape 7。其他的还有Apple的Safari 1.2,Opera 7.60,Firefox等,有提供了类似的功能。
好,让我们切开这块蛋糕看看它的实现。
XMLHttpRequest是一个客户端组件,需要在Javascript脚本中实例化后才能使用。幸运的事,这样做非常简单。在IE中,实现代码如下:
var req = new ActiveXObject("Microsoft.XMLHTTP");
对其他的浏览器,使用:
var req = new XMLHttpRequest();
你当然想在代码中实现一些判断逻辑,有很多方法可以做到这点,但是我倾向于简单的方案,比如只是检查一下某个对象是否存在:
var req;
if (window.XMLHttpRequest) { // Non-IE browsers
req = new XMLHttpRequest();
} else if (window.ActiveXObject) { // IE
req = new ActiveXObject("Microsoft.XMLHTTP");
}
if (window.XMLHttpRequest) { // Non-IE browsers
req = new XMLHttpRequest();
} else if (window.ActiveXObject) { // IE
req = new ActiveXObject("Microsoft.XMLHTTP");
}
不管你怎么实现,上面的代码执行之后,你会发现变量req现在指向了一个XMLHttpRequest对象,这个对象有一组属性和方法,列举如下:
Property Description
onreadystatechange Event handler for an event that fires at every state change
readyState Status:
0 = uninitialized
1 = loading
2 = loaded
3 = interactive
4 = complete
responseText Data returned from server in string form
responseXML DOM-compatible document object of data returned
status HTTP status code (i.e., 200, 404, 500, etc.)
statusText String message associated with the status code
Method Description
abort() Stops the current request
getAllResponseHeaders() Returns all headers (name and value) as a string
getResponseHeader( Returns the value of the specified header
"<headerName>")
open("", "URL"[, Opens a connection and retrieves response from
the specified URL.
asyncFlag[, ""[, Can also specify optional values method (GET/POST),
username and
"<password>"]]]) password for secured sites
send(content) Transmits request (can include postable string or DOM object data)
setRequestHeader Assigns values to the specifed header
("<name>", "")
继续介绍之前,我强烈建议你运行本文末尾给出的那个web应用。如果你还没有下载示例应用,请参见本文末尾给出的链接,下载并安装到你的 servlet引擎中。示例应用是以展开目录的结构形式发布的,所以只要解压后拷贝解压出的目录就可以工作。比如,如果你使用Tomcat,只要把 xhrstruts目录拷贝到\webapps下就可以了。完成之后,启动服务器即可。
该应用可以通过http://localhost:8080/xhrstruts (将8080换成你的服务器所监听的端口)来访问。 它展示了几种不同的应用场景:一个可排序的table,一个可以改变另一个下拉框内容的下拉框(如上文所述),一个RSS feed 解析器。正像本文标题中说明的那样,该示例基于struts。尽管AJAX可以完全独立于struts和任何其他的后端技术,但我使用Java,而且使用 struts,所以......
web应用中的所有例子都在代码头部的标签中包含有一段代码,尽管每个都有所不同,总体是出自相同的基础代码,如下:
var req;
var which;
function retrieveURL(url) {
if (window.XMLHttpRequest) { // Non-IE browsers
req = new XMLHttpRequest();
req.onreadystatechange = processStateChange;
try {
req.open("GET", url, true);
} catch (e) {
alert(e);
}
req.send(null);
} else if (window.ActiveXObject) { // IE
req = new ActiveXObject("Microsoft.XMLHTTP");
if (req) {
req.onreadystatechange = processStateChange;
req.open("GET", url, true);
req.send();
}
}
}
function processStateChange() {
if (req.readyState == 4) { // Complete
if (req.status == 200) { // OK response
document.getElementById("urlContent").innerHTML = req.responseText;
} else {
alert("Problem: " + req.statusText);
}
}
}
var which;
function retrieveURL(url) {
if (window.XMLHttpRequest) { // Non-IE browsers
req = new XMLHttpRequest();
req.onreadystatechange = processStateChange;
try {
req.open("GET", url, true);
} catch (e) {
alert(e);
}
req.send(null);
} else if (window.ActiveXObject) { // IE
req = new ActiveXObject("Microsoft.XMLHTTP");
if (req) {
req.onreadystatechange = processStateChange;
req.open("GET", url, true);
req.send();
}
}
}
function processStateChange() {
if (req.readyState == 4) { // Complete
if (req.status == 200) { // OK response
document.getElementById("urlContent").innerHTML = req.responseText;
} else {
alert("Problem: " + req.statusText);
}
}
}
这段代码逻辑很简单。你可以调用retieveURL()方法,传入你想访问的URL,该方法根据浏览器类型实例化相应的 XMLHttpRequest对象,开启一个对指定URL的请求。请留意这里的try...catch语句块,加入这段代码是因为有些浏览器(比如 Firefox)不允许使用XMLHttpRequest从一个域到另一个域发送请求,换句话说,如果你从 www.omnytex.com/test.htm页面请求www.cnn.com,该类浏览器是不允许的,但是,访问 www.omnytext.com/whatever.htm是可以的。IE允许这种跨域访问但是需要用户验证。
有一行代码很重要:req.onreadystatechange = processStateChange,这行代码设定了一个事件处理器。当request的状态发生变化时,processStateChange()方法将被调用。然后,你可以检查 XMLHttpRequest对象的状态进行后续处理。上面的列表中列出了所有可能的值。这里我们关心的是请求完成之后,下面要做的事就是检查收到的 HTTP响应代码,除200(HTTP OK)外的任何代码都预示着需要显示错误信息。
在这个例子中,如果响应接收完成且没有异常,我们就把接收到的代码插入urlContent span,然后最终效果就显示在页面上。
语法上讲,这就是所有XMLHttpRequest的使用方法!
另一个更有趣的例子是web应用中的第二个,动态排序table。下面是完整的页面代码:
<code>
<html>
<head>
<title>Example 2</title>
<html>
<head>
<title>Example 2</title>
<script>
var req;
var which;
function retrieveURL(url) {
if (window.XMLHttpRequest) { // Non-IE browsers
req = new XMLHttpRequest();
req.onreadystatechange = processStateChange;
try {
req.open("GET", url, true);
} catch (e) {
alert(e);
}
req.send(null);
} else if (window.ActiveXObject) { // IE
req = new ActiveXObject("Microsoft.XMLHTTP");
if (req) {
req.onreadystatechange = processStateChange;
req.open("GET", url, true);
req.send();
}
}
}
function processStateChange() {
if (req.readyState == 4) { // Complete
if (req.status == 200) { // OK response
document.getElementById("theTable").innerHTML = req.responseText;
} else {
alert("Problem: " + req.statusText);
}
}
}
</script>
var req;
var which;
function retrieveURL(url) {
if (window.XMLHttpRequest) { // Non-IE browsers
req = new XMLHttpRequest();
req.onreadystatechange = processStateChange;
try {
req.open("GET", url, true);
} catch (e) {
alert(e);
}
req.send(null);
} else if (window.ActiveXObject) { // IE
req = new ActiveXObject("Microsoft.XMLHTTP");
if (req) {
req.onreadystatechange = processStateChange;
req.open("GET", url, true);
req.send();
}
}
}
function processStateChange() {
if (req.readyState == 4) { // Complete
if (req.status == 200) { // OK response
document.getElementById("theTable").innerHTML = req.responseText;
} else {
alert("Problem: " + req.statusText);
}
}
}
</script>
</head>
<body onLoad="retrieveURL('example2RenderTable.do');">
<h1>Example 2</h1>
Dynamic table.<hr>
<p align="right"><a href="home.do">Return home</a></p><br>
This example shows how a table can be built and displayed on-the-fly by showing
sorting of a table based on clicks on the table headers.
<br><br>
<span id="theTable"></span>
<br>
</body>
</html>
</code>
<body onLoad="retrieveURL('example2RenderTable.do');">
<h1>Example 2</h1>
Dynamic table.<hr>
<p align="right"><a href="home.do">Return home</a></p><br>
This example shows how a table can be built and displayed on-the-fly by showing
sorting of a table based on clicks on the table headers.
<br><br>
<span id="theTable"></span>
<br>
</body>
</html>
</code>
请注意中几乎相同的代码。这里我们实际请求的是一个Struts的Action,该action返回绘制table的HTML脚本。还有其他方法可以达到相同的效果而无需在Action中产生HTML,但这是最快捷而且工作良好的。当页面最初载入的时候我们发送请求到Action得到一个最初的table,点击任何列标题可以将该table排序并重新绘制。
我们再来看另外一个例子,RSS feed 解析器:
<code>
<html>
<head>
<title>Example 6</title>
</head>
<html>
<head>
<title>Example 6</title>
</head>
<script>
var req;
var which;
function retrieveURL(url) {
if (url != "") {
if (window.XMLHttpRequest) { // Non-IE browsers
req = new XMLHttpRequest();
req.onreadystatechange = processStateChange;
try {
req.open("GET", url, true);
} catch (e) {
alert(e);
}
req.send(null);
} else if (window.ActiveXObject) { // IE
req = new ActiveXObject("Microsoft.XMLHTTP");
if (req) {
req.onreadystatechange = processStateChange;
req.open("GET", url, true);
req.send();
}
}
}
}
function processStateChange() {
if (req.readyState == 4) { // Complete
if (req.status == 200) { // OK response
// We're going to get a list of all tags in the returned XML with the
// names title, link and description. Everything else is ignored.
// For each that we find, we'll constuct a simple bit of HTML for
// it and build up the HTML to display. When we hit a title,
// link or description element that isn't there, we're done.
xml = req.responseXML;
i = 0;
html = "";
while (i >= 0) {
t = xml.getElementsByTagName("title")[i];
l = xml.getElementsByTagName("link")[i];
d = xml.getElementsByTagName("description")[i];
if (t != null && l != null && d != null) {
t = t.firstChild.data;
l = l.firstChild.data;
d = d.firstChild.data;
html += "<a href=\"" + l + "\">" + t + "</a><br>" + d + "<br><br>";
i++;
} else {
i = -1;
}
}
document.getElementById("rssData").innerHTML = html;
} else {
alert("Problem: " + req.statusText);
}
}
}
</script>
var which;
function retrieveURL(url) {
if (url != "") {
if (window.XMLHttpRequest) { // Non-IE browsers
req = new XMLHttpRequest();
req.onreadystatechange = processStateChange;
try {
req.open("GET", url, true);
} catch (e) {
alert(e);
}
req.send(null);
} else if (window.ActiveXObject) { // IE
req = new ActiveXObject("Microsoft.XMLHTTP");
if (req) {
req.onreadystatechange = processStateChange;
req.open("GET", url, true);
req.send();
}
}
}
}
function processStateChange() {
if (req.readyState == 4) { // Complete
if (req.status == 200) { // OK response
// We're going to get a list of all tags in the returned XML with the
// names title, link and description. Everything else is ignored.
// For each that we find, we'll constuct a simple bit of HTML for
// it and build up the HTML to display. When we hit a title,
// link or description element that isn't there, we're done.
xml = req.responseXML;
i = 0;
html = "";
while (i >= 0) {
t = xml.getElementsByTagName("title")[i];
l = xml.getElementsByTagName("link")[i];
d = xml.getElementsByTagName("description")[i];
if (t != null && l != null && d != null) {
t = t.firstChild.data;
l = l.firstChild.data;
d = d.firstChild.data;
html += "<a href=\"" + l + "\">" + t + "</a><br>" + d + "<br><br>";
i++;
} else {
i = -1;
}
}
document.getElementById("rssData").innerHTML = html;
} else {
alert("Problem: " + req.statusText);
}
}
}
</script>
<body>
<h1>Example 6</h1>
RSS example.<hr>
<p align="right"><a href="home.do">Return home</a></p><br>
This example is a more real-world example. It retrieves an RSS feed from one
of three user-selected sources, parses the feed and displays the headlines
in clickable form. This demonstrates retrieving XML from a server and
dealing with it on the client.
<br><br>
<b>Note that the RSS feed XML is actually stored as files within this
webapp. That is because some browsers will not allow you to retrieve
content with XMLHttpRequest outside the domain of the document trying to
do the call. Some browsers will allow it with a warning though.</b>
<br><br>
<form name="rssForm">
<select name="rssFeed" onChange="retrieveURL(this.value);">
<option value=""></option>
<option value="cnn_rss.xml">CNN Top Stories</option>
<option value="slashdot_rss.xml">Slashdot</option>
<option value="dans_rss.xml">Dan's Data</option>
</select>
</form>
<hr><br>
<span id="rssData"></span>
<br>
</body>
</html>
</code>
<h1>Example 6</h1>
RSS example.<hr>
<p align="right"><a href="home.do">Return home</a></p><br>
This example is a more real-world example. It retrieves an RSS feed from one
of three user-selected sources, parses the feed and displays the headlines
in clickable form. This demonstrates retrieving XML from a server and
dealing with it on the client.
<br><br>
<b>Note that the RSS feed XML is actually stored as files within this
webapp. That is because some browsers will not allow you to retrieve
content with XMLHttpRequest outside the domain of the document trying to
do the call. Some browsers will allow it with a warning though.</b>
<br><br>
<form name="rssForm">
<select name="rssFeed" onChange="retrieveURL(this.value);">
<option value=""></option>
<option value="cnn_rss.xml">CNN Top Stories</option>
<option value="slashdot_rss.xml">Slashdot</option>
<option value="dans_rss.xml">Dan's Data</option>
</select>
</form>
<hr><br>
<span id="rssData"></span>
<br>
</body>
</html>
</code>
首先要注意的是RSS feed XML文件实际上是包含在web应用中的本地文件。一个真正实用使用XMLHttpRequest的RSS阅读器是不可能实现的因为要涉及到跨域处理。然而,一个可行的方法是写一个Action从真正的URL处得到feed然后将之返回给请求页面,参见示例7。除了需要一个Action作为代理来得到 RSS feed外,页面上代码还是相同的。
上面的例子跟其它的类似,除了在事件处理器中的XML解析代码。这只是一个简化的例子,我们只是简单地忽略了除标题之外的其他标签。在一个真实的例子中(比如一个请求复杂XML的应用),解析代码会变得复杂,但这个我留给读者作为练习。
让我们以一个在请求中提交数据的例子做结,中的脚本如下:
var req;
var which;
function submitData() {
// Construct a CSV string from the entries. Make sure all fields are
// filled in first.
f = document.theForm.firstName.value;
m = document.theForm.middleName.value;
l = document.theForm.lastName.value;
a = document.theForm.age.value;
if (f == "" || m == "" || l == "" || a == "") {
alert("Please fill in all fields first");
return false;
}
csv = f + "," + m + "," + l + "," + a;
// Ok, so now we retrieve the response as in all the other examples,
// except that now we append the CSV onto the URL as a query string,
// being sure to escape it first.
retrieveURL("example5Submit.do?csv=" + escape(csv));
}
function retrieveURL(url) {
if (window.XMLHttpRequest) { // Non-IE browsers
req = new XMLHttpRequest();
req.onreadystatechange = processStateChange;
try {
req.open("GET", url, true);
} catch (e) {
alert(e);
}
req.send(null);
} else if (window.ActiveXObject) { // IE
req = new ActiveXObject("Microsoft.XMLHTTP");
if (req) {
req.onreadystatechange = processStateChange;
req.open("GET", url, true);
req.send();
}
}
}
function processStateChange() {
if (req.readyState == 4) { // Complete
if (req.status == 200) { // OK response
document.getElementById("theResponse").innerHTML = req.responseText;
} else {
alert("Problem: " + req.statusText);
}
}
}
var which;
function submitData() {
// Construct a CSV string from the entries. Make sure all fields are
// filled in first.
f = document.theForm.firstName.value;
m = document.theForm.middleName.value;
l = document.theForm.lastName.value;
a = document.theForm.age.value;
if (f == "" || m == "" || l == "" || a == "") {
alert("Please fill in all fields first");
return false;
}
csv = f + "," + m + "," + l + "," + a;
// Ok, so now we retrieve the response as in all the other examples,
// except that now we append the CSV onto the URL as a query string,
// being sure to escape it first.
retrieveURL("example5Submit.do?csv=" + escape(csv));
}
function retrieveURL(url) {
if (window.XMLHttpRequest) { // Non-IE browsers
req = new XMLHttpRequest();
req.onreadystatechange = processStateChange;
try {
req.open("GET", url, true);
} catch (e) {
alert(e);
}
req.send(null);
} else if (window.ActiveXObject) { // IE
req = new ActiveXObject("Microsoft.XMLHTTP");
if (req) {
req.onreadystatechange = processStateChange;
req.open("GET", url, true);
req.send();
}
}
}
function processStateChange() {
if (req.readyState == 4) { // Complete
if (req.status == 200) { // OK response
document.getElementById("theResponse").innerHTML = req.responseText;
} else {
alert("Problem: " + req.statusText);
}
}
}
在这个例子中,我们只是简单地用用户的输入创建了一个以逗号分割的字符串。你当然可以创建一个XML文档然后提交,事实上那是更常见的情况。但是这正是我不想那样做的一部分原因:我想告诉读者你并不一定非要使用XMLHttpRequest对象来传输 XML。就本例而言,除了将一个CSV字符串添加到URL之外并没有做任何事情。在网上有不计其数的例子演示了如何创建XML文档并使用 XMLHttpRequest.send()方法提交,我强烈推荐你阅读相关文档,当然,如果你使用这种方法的话。
我希望这篇简短的文章和附加的例子可以给你一个好的研究XMLHttpRequest对象的起点。在结束之前我还想说AJAX概念本身并不强制你使用XMLHttpRequest对象,你可以使用其他方法得到相同的效果,比如代替 XMLHttpRequest的隐藏frame,这正是我在本文开头提到的在五年前的那个项目中采用的方法。然而,XMLHttpRequest的确使得 AJAX概念更容易实现,而且更标准。请参见Google研究这种技术的强大之处。
但是,我提醒所有认为全部的web应用都应该用这种方法开发的人,我不认为这是web开发的不二法门。在某些情况下它是一个好的方案,但在其他情况下不是。如果获得尽可能多的浏览者是你的目标,你最好放弃这种方案。如果一个用户取消了浏览器的脚本解释功能(而你的网站除了这又没有其他出彩的地方),这就不是一个好的情况。还有其他AJAX不适用之处,但是你完全可以把它当成你工具箱中的一个普通工具:它适合某些工作,不适合其他工作。毕竟,你不能指望用一个胶水枪钉钉子吧?
到此为止,我希望本文已经带给了你进一步思考的食粮,尽情享用吧!