3.1 通信控制模式
从第2章中,你已经掌握了如何用JavaScript与服务器进行通信。现实的问题是:启动并连续向服务器发送请求的最佳方法是什么?有些情况下,最好是从服务器预载入一些信息,以便能够快速响应用户的操作;而在另外一些情况下,可能想在不同的时间间隔内,向服务器发送数据或从服务器接收数据。或许所有的东西并非必须立刻下载,而可能按一个特定的顺序来下载。对于客户端与服务器的通信,Ajax提供了精细的控制,能够实现预想的行为。
3.1.1 预先获取
在传统的Web解决方案中,应用程序并没有下一步要做什么的想法。页面中包含一些链接,每一个链接都指向网站的不同部分。这也可以称为“按需获取”(fetch on demand),根据用户的操作,服务器就可以准确地获知需要获取的数据。虽然从一开始Web就是以这种范式定义的,但这对于“开始—结束”的用户交互模式而言则有不利的一面,但有了Ajax的帮助,就可以改变这一局面。
预先获取(Predictive Fetch)模式的概念很简单,实现却不容易:Ajax应用程序必须猜测用户下一步要做什么,然后获取相应的数据。最理想的情况是,最好总是知道用户下一步将要做什么,并且确得下一步的数据当需要时就已经准备好了。但是在现实中,判断用户的后续操作却只是一个根据你的意愿的猜谜游戏。
也有一些使用场景是比较容易预测用户下一步操作的。假设你正在阅读一篇分为三页的在线文章。其潜在的逻辑就是如果你对阅读第一页感兴趣,那么就会对阅读第二页和第三页感兴趣。因此如果第一页已经载入了几秒钟(通过超时来判断是很简单的),那么在后台下载第二页就是很安全的。同样,如果第二页载入了几秒钟,同样的逻辑可以推测读者会继续阅读第三页。当这些额外的数据已经下载并且缓存在客户端,那么用户就几乎可以在点击“下一页”按钮后立即看到下一页的内容。
另一个简单的使用场景是撰写电子邮件的过程。大多数情况下,你所撰写的电子邮件将发送给一个你认识的人,因此可以假定该人的信息已经存在于你的地址薄中。要想提供帮助,可以在后台预先载入你的地址簿并提供建议。该方法在许多基于Web的电子邮件系统中经常采用,包括Gmail和AOLWebmail。关键还是“合理假设”准则。预测并预载入与用户可能的下一步相关的信息,可以使应用程序更轻快、反应更迅速;使用Ajax获取与任何可能的下一步相关的信息,那么很快会使服务器超负荷运转,使浏览器陷入额外的处理中。经验表明,只有从逻辑上确认该信息是用户下一步请求必需的,才预先获取它。
3.1.2 页面预载入实例
正如前面所提到的,预先获取模式的最简单也是最多的应用是预载入在线文章的后续页。随着Weblog(或简写为blog)的出现,好像所有的人都迷上了发表文章,都在自己的网站写作。在线阅读长文章对眼睛来说是件费力的事,因此大多数网站都会将其分成多个页。这样有利于阅读,但会使得内容的载入时间更长,因为要对每个新页面都需要处理其格式、菜单以及原始页中的广告。而预先获取模式则可以在读者还在阅读第一页时就在客户端和服务器载入下一页面中的文本信息。
最开始,我们需要一个处理页面预装载的服务器端逻辑。下面的ArticleExample.php文件中就包含了在线显示文章的代码:
<?php
$page = 1;
$dataOnly = false;
if (isset($_GET["page"])) {
$page = (int) $_GET["page"];
}
if (isset($_GET["data"]) && $_GET["dataonly"] == "true") {
$dataOnly = true;
}
if (!$dataOnly) {
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Article Example</title>
<script type="text/javascript" src="zxml.js"></script>
<script type="text/javascript" src="Article.js"></script>
<link rel="stylesheet" type="text/css" href="Article.css" />
</head>
<body>
<h1>Article Title</h1>
<div id="divLoadArea" style="display:none"></div>
<?php
$output = "<p>Page ";
for ($i=1; $i < 4; $i++) {
$output .= "<a href=\"ArticleExample.php?page=$i\" id=\"aPage$i\"";
if ($i==$page) {
$output .= "class=\"current\"";
}
$output .= ">$i</a> ";
}
echo $output;
}
if ($page==1) {
echo $page1Text;
} else if ($page == 2) {
echo $page2Text;
} else if ($page == 3) {
echo $page3Text;
}
if (!$dataOnly) {
?>
</body>
</html>
<?php
}
?>
默认情况下,该文件将显示文章内容的第一页。如果指定了查询字符串参数page的值,例如page=2,那么将显示文章的指定页。当查询字符串中包含dataonly=true时,则页面只输出一个包含文章指定页内容的<div/>元素,与page参数结合使用,可以获取文章中的任意一页。
在该页面中的HTML为文章标题预留了位置,同时还包括一个用来载入额外页面的<div/>元素。这个<div/>元素首先将display属性设置为none,以确保不会显示意外的内容。紧接其后的PHP代码所包含的程序逻辑就是负责输出该文章的所有页码。在本例中,有三页内容,因此在顶部输出了三个链接(如图3-1所示)。
当前页码的CSS类指定为current,因而用户知道现在查看的是哪一个页面。该类在Article.css中定义为:

图 3-1
a.current {
color: black;
font-weight: bold;
text-decoration: none;
}
当读者阅读一个指定页时,指向该页的链接将变成黑色、粗体,并且去掉了下划线,这样就清晰地标识出了其当前所阅读的页。默认情况下,这些链接所调用的是相同的页,只不过修改了查询字符串中的page参数的值;大多数网站都是采用这种方法来处理多页文章的。但是,使用预先获取模式可以改进用户体验,提高访问的速度。
在本例中,实现预先获取模式还需要设置一些JavaScript全局变量:
var oXmlHttp = null; //XMLHttp对象
var iPageCount = 3; //总页数
var iCurPage = -1; //当前显示页
var iWaitBeforeLoad = 5000; //载入新页前等待的时间(单位为ms)
var iNextPageToLoad = -1; //要下载的下一页
第一个变量是全局的XMLHttp对象,它用来发送所有请求以获得更多的信息。第二个参数是iPageCount,它用来存储该文章的总页数(这里采用了硬性编码,但实际应用中它应该是生成的)。变量iCurPage则用来存储当前显示给用户的页码。接下来的两个变量直接处理数据预载入:iWaitBeforeLoad表示在载入下一页之前等待的毫秒数,而iNextPageToLoad则表示当指定时间结束应该载入的页码。在这个例子中,5秒钟(5000毫秒)之后就会载入新页,这个时间足够让读者阅读文章的前几句内容,并判断是否有兴趣阅读剩下的内容。如果读者在5秒钟之内就离开了,说明他对文章的其他部分没有兴趣。
要完成这个处理,首先需要一个函数来确定将要获取的指定页面的URL。这个getURLForPage()函数有一个参数,用来指定你想要获取的页码。然后,将提取当前URL并将page参数附在最后:
function getURLForPage(iPage) {
var sNewUrl = location.href;
if (location.search.length > 0) {
sNewUrl = sNewUrl.substring(0, sNewUrl.indexOf("?"))
}
sNewUrl += "?page=" + iPage;
return sNewUrl;
}
该函数首先从location.href中抽取URL,但它是针对该页面的完整URL,可能包括其中的查询字符串。接着测试这个URL,通过location.search的值是否大于0(location.search返回的只是查询字符串,如果有一个指定,则将包含?符号)来判断是否有查询字符串。如果有查询字符串,则使用substring()方法将其去除。然后将page参数附到URL后面并将其返回。这个函数将在许多不同的地方应用。
接下来的函数是showPage(),你或许猜得到,它负责显示文章的下一页:
function showPage(sPage) {
var divPage = document.getElementById("divPage" + sPage);
if (divPage) {
for (var i=0; i < iPageCount; i++) {
var iPageNum = i+1;
var divOtherPage = document.getElementById("divPage" + iPageNum);
var aOtherLink = document.getElementById("aPage" + iPageNum);
if (divOtherPage && sPage != iPageNum) {
divOtherPage.style.display = "none";
aOtherLink.className = "";
}
}
divPage.style.display = "block";
document.getElementById("aPage" + sPage).className = "current";
} else {
location.href = getURLForPage(parseInt(sPage));
}
}
该函数首先检查指定页是否已经载入了一个<div/>元素,<div/>元素将以divPage加上页号进行命名(例如,divPage1为第一页,divPage2为第二页,等等)。如果这个<div/>元素已经存在,那么说明该页已经预先获取了,因此只需切换当前显示的页即可。我们将遍历所有的页,然后将除了sPage参数中指定的页之外的其他页都隐藏。同时,将每个页的链接的CSS类指定为空字符串。然后,将当前页面的<div/>元素的display属性设置为block以显示它,然后再将该页面链接的CSS类设置为current。
如果<div/>元素不存在,那么将通过获取URL(使用前面定义的getURLForPage()函数)和赋给location.href,以传统方式访问下一页。当用户在5秒钟之内点击页面的连接,其用户体验就与传统的Web范式相同了。
loadNextPage()函数用来在后台载入每个新的页面。该函数确保执行对有效页的请求,同时保证按顺序并在指定的时间间隔获取页面:
function loadNextPage() {
if (iNextPageToLoad <= iPageCount) {
if (!oXmlHttp) {
oXmlHttp = zXmlHttp.createRequest();
} else if (oXmlHttp.readyState != 0) {
oXmlHttp.abort();
}
oXmlHttp.open("get", getURLForPage(iNextPageToLoad)
+ "&dataonly=true", true);
oXmlHttp.onreadystatechange = function () {
//更多代码
};
oXmlHttp.send(null);
}
}
该函数首先通过与iPageCount进行比较,以确保存在iNextPageToLoad中的页码是有效的。通过了这个检查后,下一步就是检查全局XMLHttp对象是否已经创建。如果没有,则使用zXml库的createRequest()方法来创建。如果已经实际化,则检查readyState属性确保其值为0。如果readyState不为0,那么必须调用abort()方法来重新设置XMLHttp对象。
接下来,调用open()方法,指明请求将为异步的GET请求。使用getURLForPage()函数来获取该URL,然后附上字符串“&dataonly=true”,确保只返回页面的文本信息。设置完这些信息后,则转到onreadystatechange事件处理函数。
在本例中,onreadystatechange事件处理函数负责获取文章的文本内容,同时创建相应的DOM结构以显示它:
function loadNextPage() {
if (iNextPageToLoad <= iPageCount) {
if (!oXmlHttp) {
oXmlHttp = zXmlHttp.createRequest();
} else if (oXmlHttp.readyState != 0) {
oXmlHttp.abort();
}
oXmlHttp.open("get", getURLForPage(iNextPageToLoad)
+ "&dataonly=true", true);
oXmlHttp.onreadystatechange = function () {
if (oXmlHttp.readyState == 4) {
if (oXmlHttp.status == 200) {
var divLoadArea = document.getElementById("divLoadArea");
divLoadArea.innerHTML = oXmlHttp.responseText;
var divNewPage = document.getElementById("divPage"
+ iNextPageToLoad);
divNewPage.style.display = "none";
document.body.appendChild(divNewPage);
divLoadArea.innerHTML = "";
iNextPageToLoad++;
setTimeout(loadNextPage, iWaitBeforeLoad);
}
}
};
oXmlHttp.send(null);
}
}
正如上一章中所阐述的,将检查readyState属性什么时候等于4,以及检查status属性确保没有错误。当传入这两个条件后,就会开始实际的处理。首先,获取载入区域的<div/>元素的引用,并将其存于divLoadArea变量中。然后,将请求的responseText赋给载入区域的innerHTML属性。由于返回的文本信息是一个HTML片段,将其解析后将创建相应的DOM对象。接下来,获取包含下一页内容(将divPage加上iNextPageToLoad就可以获取其ID)的<div/>元素的引用,以及将<div/>元素的display属性设置为0,以确保其移出载入区域时隐藏起来。紧接的后一句将divNewPage附加到文档的主体,为了便于使用将其放到正式的视图区域。然后将载入区域的innerHTML属性设置为空字符串,为载入下一页做好准备。在此之后,当指定的时间间隔过后iNextPageToLoad变量的值将加1,并且超时时间值也将重新设置为原值,以便再次调用该函数。该函数将每5秒钟执行一次,直到所有的页面都载入为止。
由于该页面没有JavaScript也能工作,因此在确定浏览器支持XMLHttp后再附加这些代码。幸运的是,在zXml库中,zXmlHttp对象提供了一个名为isSupported()的函数,它可以用来完成这种检查:
window.onload = function () {
if (zXmlHttp.isSupported()) {
//在此开始编写Ajax代码
}
};
这个代码块之中是预先获取模式的代码,这样就可以确保浏览器不支持XMLHttp,也不会因功能不完整的代码而对可用性产生负面影响。
在对文章设置预先获取模式时的第一步是决定用户当前阅读的是哪一页。要实现这一目标,必须检查URL的查询字符串,看是否指定了page参数。如果有,则可以从中获取页码;否则就可以假设页码为1(默认值):
window.onload = function () {
if (zXmlHttp.isSupported()) {
if (location.href.indexOf("page=") > -1) {
var sQueryString = location.search.substring(1);
iCurPage = parseInt(sQueryString.substring(sQueryString.indexOf("=")+1));
} else {
iCurPage = 1;
}
iNextPageToLoad = iCurPage+1;
//更多代码
}
};
在这段代码中,将测试页面的URL(通过location.href可访问)是否指定了page=。如果有,则使用location.search(它只返回查询字符串,它的最前面是一个“?”,可以通过调用substring(1)去除这个符号)来获得其查询字符串。紧接着的一行则是用来获取查询字符串中“=”号之后的内容(也就是页码),然后使用parseInt()将其转成整型,并将结果存入iCurPage变量中。另外,如果在查询字符串中没有指定page参数,则可以假设它是第一页,因此将iCurPage变量值赋为1。这段代码的最后一行,则是将iNextPageToLoad变量的值设置为当前页码加1,确保不会重新载入已经存在的数据。
下一步则是重载处理页面链接的函数。记住,默认情况下这些链接将指向相同的页面,只是查询字符串设置为将要显示的页码。如果支持XMLHttp,就需要重载其行为并替换为调用Ajax功能的函数:
window.onload = function () {
if (zXmlHttp.isSupported()) {
if (location.href.indexOf("page=") > -1) {
var sQueryString = location.search.substring(1);
iCurPage = parseInt(sQueryString.substring(sQueryString.indexOf("=")+1));
} else {
iCurPage = 1;
}
iNextPageToLoad = iCurPage+1;
var colLinks = document.getElementsByTagName("a");
for (var i=0; i < colLinks.length; i++) {
if (colLinks[i].id.indexOf("aPage") == 0) {
colLinks[i].onclick = function (oEvent) {
var sPage = this.id.substring(5);
showPage(sPage);
if (oEvent) {
oEvent.preventDefault();
} else {
window.event.returnValue = false;
}
}
}
}
setTimeout(loadNextPage, iWaitBeforeLoad);
}
};
在这里,将使用getElementsByTagName()来获取链接(<a/>元素)的集合。如果该链接拥有一个以aPage开头的ID,那么这是一个页面链接并且需要指定;这将使用indexOf()并检查其值是否为0来确定。其值为0将表明aPage是该字符串的第一部分。接下来,将为链接指定一个onclick事件处理函数。在这个事件处理函数中,页码将使用链接的ID(通过this.id可访问)来获取,并使用substring()来返回aPage后的信息。然后,这个值将传给本节前面定义的showPage()函数,它将显示相应的页。在此之后,还必须考虑如何去除链接的默认行为,即指向一个新页。由于这在IE和DOM事件模型中存在一些区别,因此需要一个if语句来确定采用的是什么样的操作。如果已经向函数传入了event对象(参数oEvent),那么就说明是一个兼容DOM的浏览器,可以通过调用preventDefault()方法来屏蔽默认的行为。但如果oEvent的值为null,那么意味着浏览器是IE,需要通过window.event来访问这个event对象。这时就需要通过将returnValue属性设置为false来屏蔽默认行为,这是IE所采用的方法。
当恰当地处理了这些链接后,则为启动loadNextPage()创建了一个超时值。5秒钟之后将第一次调用该函数,而后每过5秒种再自动执行一次。
当你自己测试这个功能时,应在不同的时间段点击页面链接。如果在5秒钟之内点击,会看到页面转到了一个新的URL,其查询字符串发生了变化。下一次,你在约10秒钟后点击页面链接,你会发现文字信息发生了变化,但URL没变(同时会发现其响应速度明显比转向一个URL要快得多)。







