摘要
Ajax应用程序由于其丰富的功能、交互性以及快速的响应能力而得到人们的普遍赞许。它可以使用XMLHttpRequest对象动态地加载数据,而不是加载新的页面。在它大肆进行宣传以及许多人兴奋的同时,有评论指出,Ajax应用程序丢失了浏览器的一些重要功能,包括对后退前进按钮的支持。
本文将首先阐明为什么在Ajax应用程序中除非显式地构建后退/前进按钮以及其它浏览器功能,否则它们将无法运行的原因。然后,我们将简要介绍开发人员如何解决这些问题。最后,我们将看到有关Backbase Ajax引擎如何支持后退/前进按钮以及其它标准浏览器功能的详细情况。
Ajax应用程序是否需要后退按钮?
Ajax承诺,可以让开发人员完全基于标准的Web浏览器技术(通常是指DHTML)创建在视觉上吸引人的、高度交互式的Web应用程序。
以前开发人员不得不在功能丰富(具有高度交互性的、吸引人的用户界面)和易于到达(不需要进行客户端安装就可以工作在所有Web浏览器下的前端)二者之中作出选择。而Ajax应用程序应该能够产生既“功能丰富”又“易于到达”的前端。
但是一个界面怎样才算是“功能丰富”的,而一个应用程序又怎样才是“易于到达”的呢?
很难精确地定义“功能丰富”的含义,但是却很容易直觉地认识到:当您看到一个界面时,您就会知道它是不是功能丰富的。象Microsoft Office之类的桌面应用程序就是功能丰富的。功能丰富的界面使用诸如选项卡和上下文菜单这样的高级UI控件。这样的界面提供一些高级交互方法。例如,拖放、对关注的UI元素进行高亮显示等。传统的浏览器应用程序是功能不丰富的。它们仅限于诸如表单之类的简单控件,交互主要是由到新页面的单击链接组成。我们只要看看微软的电子邮件客户端就可以看出功能丰富和功能不丰富的区别:Outlook是功能丰富的,而Hotmail就是功能不丰富的。
Ajax应用程序已经由于功能丰富而得到人们的普遍赞许。Google的Gmail就是其中最具代表性的例子。Google所开发的其它Ajax应用程序(Google Suggest、 Google Map)、微软即将推出的名为“Kahuna”的Web邮件客户端以及Backbase RSS Reader都包含了一些高级控件和交互模块。可查看Dan Grossman的Top 10 Ajax Applications列表,其中给出了一个让人印象深刻的功能丰富界面的列表。
通过前面的讨论,可以说Ajax应用程序很明显满足“功能丰富”的标准。那么它是不是“易于到达”的呢?
首先,最基本的是,只有界面在Web浏览器中运行的应用程序才是“易于到达”的。Ajax应用程序是基于浏览器标准的,因此可以通过Web浏览器来访问。
但是,仅仅可以通过Web浏览器访问还不够。Jakob Nielson在他2000年的文章Flash: 99% Bad中指出,Flash“打破了Web的基本交互方式”。终端用户希望在使用Web应用程序时所面对的是特定的交互方式。应用程序需要遵从传统的Web交互方式,并提供以下的可用功能:
后退和前进按钮可以正常工作,以便终端用户可以导航到历史记录页面。
用户应该可以创建书签。
支持深链接,以保证终端用户可以将这个页面通过电子邮件发送给朋友和同事。
刷新按钮可以正常工作,以便刷新当前的状态而不是重新初始化应用程序。
开发人员可以使用“查看源文件”看到源代码。
终端用户可以使用“查找”对页面进行搜索。
搜索引擎可以为页面做索引并创建到搜索项的深链接。
再看一下Top 10 Ajax Applications列表,我们可以看出,之前讨论的大多数Ajax应用程序的确打破了标准的Web交互方式。在下一节中,我们将讨论为什么许多Ajax应用程序会这么做。
为什么Ajax应用程序常常会使后退按钮无法正常工作?
我们所说的Web基于以下三个原则:
使用 (D)HTML来定义界面
使用HTTP实现客户端与服务器间的通讯
使用URI进行寻址
Ajax编程突破了由以上原则所带来的种种限制,使得界面功能更加丰富。正如我在以前的文章A Backbase Ajax Front-end for J2EE Applications(中文版)中所介绍的那样,Ajax广泛使用了JavaScript(“J”)以创建功能丰富的UI组件和交互性。Ajax还引入了异步的XML通信(“A”和“X”),也就是使用XMLHttpRequest对象导入新的数据和表示逻辑而不必刷新页面。然而,目前的Ajax模型并没有解决如何处理URI的问题。
Ajax应用程序对(D)HTML和HTTP的使用方式做了改变,而这种改变带来的直接结果就是后退按钮和Web的基本交互方式的其它元素无法正常工作了。在本节的其余部分,我将说明如何通过以Ajax的方式处理URI来解决上述问题。首先我们来看看在传统的Web应用程序中URI是如何与用户交互相关联的。
从技术方面来说,用户交互是指用户界面状态的一次更改。状态改变由终端用户发起。浏览器客户端通过向服务器发出页面请求来处理状态更改(REST法则)。服务器将发送新的页面和新的URI到客户端以生成新的界面状态。
简单地说,每个用户交互都是通过会导致如下结果的服务器往返来处理的:
生成新的页面
生成新的URI
这些Web功能之所以能够被使用,是因为浏览器在它的历史记录堆栈中记录了连续的URI,并在地址栏中向终端用户显示当前URI,用户可以通过地址栏复制URI,并将其发送给朋友。当用户单击后退按钮或者向浏览器的地址栏中粘贴一个来自于电子邮件的URI时,就会触发一次到服务器的往返。因为服务器负责状态管理,所以它就可以生成相应的页面。
Ajax应用程序与传统的Web应用程序之间的主要区别在,Ajax应用程序可以处理用户的交互而无需页面重新加载。例如,通过XMLHttpRequest对象从服务器载入数据,或者使用JavaScript来处理拖放客户端。
在上面的两个例子中,状态改变了,但是却没有生成新的URI。因此,单击后退按钮或刷新按钮会产生意外的结果,在地址栏中也不会有深链接的URI。
为了提供传统的Web可用功能,Ajax应用程序需要以类似于服务器处理传统的Web应用程序的方式来处理URI客户端。Ajax应用程序需要实现以下功能:
当客户端状态发生改变时,生成一个URI,并将其发送到浏览器
当浏览器请求新的URI时可以重新创建状态。
实现以上功能后,浏览器的历史记录就可以正常工作,浏览器的地址栏就可以显示URI,当然,您也就可以将它发送给朋友了。
这里还有另外一个难点,那就是如何确定Ajax引擎什么时候需要实现以上功能(例如,哪一次状态改变需要创建新的URI)。在传统的模式中,每一次页面刷新都对应着一次URI更新。而在Ajax模式中,每一个客户端事件都将新的URI添加到浏览器堆栈中。交互设计者和开发人员将不得不做出决定:哪一次状态改变是有意义的。只对有意义的状态改变才需要生成URI。
下面我们对提供Web可用功能的Ajax应用程序在客户端需要实现的功能做一下总结:
创建历史记录
保存有意义的状态
生成相应的URI
将这个URI添加到浏览器的堆栈中
恢复历史记录
检测URI的改变
通过URI重新创建状态
在Ajax中支持后退按钮的基本设计思想
在这一节中,我们将讨论在Ajax应用程序中支持后退按钮所需的基本步骤,并给出说明所需步骤的简单示例代码。有关如何以跨浏览器兼容的方式实现后退按钮支持的更完整讨论可以参见Mike Stenhouse在Content with Style以及Brad Neuberg在OnJava发表的文章。这两篇文章也是我比较喜欢的。
简单示例程序如图1所示,在界面中将有一个选择框,它有两个值:“Year 1”和“Year 2”。对于这个程序,我们将在选择框值发生改变时跟踪历史记录。这意味着用户可以首先选择“Year 2”然后单击后退按钮后退到先前的选择。
图1.带有选择框的简单示例程序
示例程序最初是一个带有JavaScript getter和setter(用于选择框值)的简单HTML表单:
<html>
<head>
<script language="JavaScript" type="text/JavaScript">
function reportOptionValue()
{
var myForm = document.make_history;
var mySelect = myForm.change_year;
return mySelect.options[mySelect.selectedIndex].value;
}
function setOptionValue(value)
{
var myForm = document.make_history;
var mySelect = myForm.change_year;
mySelect.options[value-1].selected = true;
}
</script>
</head>
<body>
<form name=make_history>
<select name=change_year>
<option value="year_1">Year 1</option>
<option value="year_2">Year 2</option>
</select>
</form>
</body>
</html>
我们将首先实现第一个要求:创建状态的历史记录。正如我们前面所提到的,这个要求包含以下三个步骤:
创建历史记录
保存有意义的状态
生成相应的URI
将这个URI添加到浏览器的堆栈中
我们希望能够保存选择框的每一次更改。因此我们将创建新的包含选择框状态信息的URI。
为了遵循Internet标准,我们将使用URI的碎片标识符部分。按照IETF RFC 3986的规定,“……作为客户端间接引用的主要形式,碎片标识符在信息检索系统中起着特殊的作用,〈……〉碎片标识符在解除引用之前与URI的其余部分是分离的,因此,碎片本身中的标识信息只被用户代理所废弃,而不考虑URI方案……”。
使用碎片标识符,我们可以创建一个“Ajax-URI”,其中的客户端部分和服务器端部分使用“#”隔开。
JavaScript提供了window.location()函数,以便通过URI更新浏览器的历史记录和地址。此外,我们可以使用window.location.hash()直接访问碎片标识符。
在下面的代码片断中,您可以看到如何通过对选择框使用onchange事件处理程序来扩展我们的代码,该处理程序使用一个“Ajax-URI”来更新浏览器历史记录及地址栏。
<html>
<head>
<script language="JavaScript" type="text/JavaScript">
function makeHistory(newHash)
{
window.location.hash = newHash;
}
function reportOptionValue()
{
var myForm = document.make_history;
var mySelect = myForm.change_year;
return mySelect.options[mySelect.selectedIndex].value;
}
function setOptionValue(value)
{
var myForm = document.make_history;
var mySelect = myForm.change_year;
mySelect.options[value-1].selected = true;
}
</script>
</head>
<body>
<form name=make_history>
<select name=change_year
onchange=
"return makeHistory(reportOptionValue())">
<option value="year_1">Year 1</option>
<option value="year_2">Year 2</option>
</select>
</form>
</body>
</html>
正如我们在图2中所看到的,选择框的每一次变动都将导致浏览器地址的更新。请注意,在需要使用隐藏帧以获取正确的行为的Internet Explorer (IE)中会存在一些问题,详细情况还是请参见Mike Stenhouse和Brad Neuberg的文章。
图2.状态变化时历史记录堆栈被更新
我们现在有了一个在选择框的值发生变化时创建新URI的事件处理程序。新URI使用碎片标识符存储重新创建先前状态所需的信息。现在我们可以着手实现下一个功能了。
恢复历史记录
检测URI的更改
通过URI重新创建状态
在第一步中,我们通过window.location.hash()函数更新了客户端的URI。这个调用并不会产生服务器的往返,也不会导致页面刷新。因此,我们需要使用Ajax的方法(在客户端)处理URI的改变。
首先需要增加一个轮询函数,以定时检查浏览器历史记录中的URI。我将在页面的onload事件中使用pollHash()函数,每隔1000毫秒它将重新执行一次。
这个轮询函数将调用handleHistory()函数,后者检查在上一次检查之后URI是否改变了。我们将借助一个名为expectedHash的全局变量来实现。
最后一部分是确定URI是否发生了改变,这种改变由选择框中的事件处理程序引起,或者是因为终端用户单击了后退按钮而造成。我们通过在选择框的事件处理程序中设置expectedHash来达到此目的。
<html>
<head>
<script language="JavaScript" type="text/JavaScript">
var expectedHash = "";
function makeHistory(newHash)
{
window.location.hash = newHash;
expectedHash = window.location.hash;
return true;
}
function reportOptionValue()
{
var myForm = document.make_history;
var mySelect = myForm.change_year;
return mySelect.options[mySelect.selectedIndex].value;
}
function setOptionValue(value)
{
var myForm = document.make_history;
var mySelect = myForm.change_year;
mySelect.options[value-1].selected = true;
return true;
}
function handleHistory()
{
if ( window.location.hash != expectedHash )
{
expectedHash = window.location.hash;
var newoption = expectedHash.substring(6);
setOptionValue( newoption );
}
return true;
}
function pollHash() {
handleHistory();
window.setInterval("handleHistory()", 1000);
return true;
}
</script>
</head>
<body language="JavaScript"
onload="return pollHash()">
<form name=make_history>
<select name=change_year
onchange="return makeHistory(reportOptionValue())">
<option value="year_1">Year 1</option>
<option value="year_2">Year 2</option>
</select>
</form>
</body>
</html>
到此,我们的示例程序就完成了。在这个程序中,我们演示了如何在URI中记录状态,如何将URI添加到浏览器的历史记录堆栈中,如何从后退按钮检测地址变动,以及最终如何重新创建所需的状态。
这个示例程序还缺少以下功能:
对使用隐藏帧的IE的支持
更多的固定URI(这个示例程序只用于选择框选项少于10的情况)
在构造时注册初始状态
以一种兼容所有浏览器的健壮方式实现对所有传统的Web可用功能的处理不是一件容易的事。一种替代方法是使用对这些功能提供了内置支持的Ajax工具包。
在下一节中,我们将描述Backbase Ajax引擎如何提供这些功能。我参考了Ajax forum on the Backbase DevNet上的实现。
案例分析:包含后退按钮和深链接的Ajax论坛
Backbase Ajax引擎是一个成熟的、功能丰富的Ajax软件包。对所有传统Web可用功能的支持是Backbase的优点之一。
Backbase DevNet包含了为开发人员提供的、与Backbase和Ajax有关的信息。而开发人员论坛是DevNet的一部分。
Backbase Web应用程序(包括DevNet及其讨论论坛)是使用Backbase构建的。为了演示该论坛功能丰富和易于到达的特点,我们将逐步遍历论坛的典型用例:
开发人员浏览论坛,阅读不同的主题。
开发人员复制这个主题的URI,将其粘贴到电子邮件中并发送给朋友。这个朋友从电子邮件中复制这个URI到一个浏览器中并打开同一论坛主题。
开发人员单击后退按钮以阅读以前的主题。
进行几次用户交互后的论坛界面状态
我们来看看开发人员来到“BXML”论坛并选中名为“Issue with vertical and horizontal menus”的贴子之后,论坛界面的状态以及地址栏中的对应URI是什么样的情况。
论坛和贴子被选中,并被高亮显示。讨论的主题被显示出来以供阅读。在URI的碎片标识符中包含了所有的相关信息。在#后面,我们看到了为书签和深链接而记录的完整状态:“forum”表示开发人员在浏览这个Web站点的论坛部分;“forum=2”表示当前选中的是BXML论坛,“thread=211”记录了当前所选择的主题。最后,方括号中的“[5]”表示与书签结合的对多个后退和前进步骤的处理。
图3.具有Ajax URI的论坛初始状态 (单击图片查看大图)
访问Backbase论坛,您就可以看到URI如何随着每次状态改变而更新,即使更新是在客户端进行处理的,或者牵涉到通过XMLHttpRequest对象进行部分页面更新。
在新的浏览器窗口内重新创建论坛界面的状态
现在让我们看看当开发人员将当前URI发送给朋友时会发生什么情况。这个朋友在浏览器窗口中打开了这个URI,期望能看到相同的界面状态。需要在新的浏览器中重新创建该状态。对于本文,我是从一个Firefox窗口中复制URI到一个新打开的IE窗口中。
在地址栏中输入URI首先会产生一个服务器端的请求。使用“#”前的部分,会加载Backbase.com,在这一过程中,Backbase Ajax引擎也就实现了初始化。活动的Backbase引擎会阅读URI中“#”后的部分。通过这些信息,Backbase引擎会转到“论坛(forum)”部分,并选定BXML论坛(id=2)中的第211个主题,从而创建相应的状态。不需要页面的刷新,只需从服务器中加载附加的内容并在客户端部分地更新界面,就可以实现了。
在后续的浏览器功能的处理中,新的URI被添加到浏览器历史记录中,这个新的URI既可以在地址栏中使用,也可以用来做深链接。“[0]”表示没有可返回(使用后退按钮)的先前状态。
图4.在新的浏览器窗口中重新创建论坛状态(单击图片查看大图)
用户单击后退按钮后的论坛界面状态
第一步我们研究了URI如何随着由用户交互所触发的界面状态更改而更新。下面我们将看到相反的情况:用户请求新的URI,相应的状态被重新创建。
通过单击后退按钮,用户要求返回先前阅读的页面。浏览器通过从历史记录堆栈中找回先前的URI来响应后退按钮。Backbase Ajax引擎将监测这一变化,从历史记录中读取新的URI,并来到“论坛”部分选定BXML论坛(id=2)中的第192个主题,从而重新构建相应的状态。新的URI将按照上述语义显示在地址栏中。
到这里,我们的案例分析也就结束了。
图5.单击后退按钮后的论坛状态(单击图片查看大图)
Ajax程序确实需要后退按钮!
在过去的几年中,Web开发人员因为市场要求“易于到达”并愿意接受“功能丰富”方面的牺牲,所以选择构建Web界面。然而,当前Ajax受到的普遍关注清楚地显示出这种情况实际上只是暂时的。市场现在强烈要求Web程序也能像桌面应用程序那样具有丰富的功能、交互性以及敏捷的响应能力。
但是,终端用户已经习惯了Web交互方式。使用常见模式与任何Web界面进行交互可以提高生产力。终端用户期望后退/前进按钮和刷新按钮能正常工作,可以创建书签和深链接,可以查看源文件,使用“查找”对页面进行搜索,而且搜索引擎可以对Ajax应用程序建立索引。
Ajax社区必须知道:正如本文所述,在Ajax应用程序中提供对后退/前进按钮以及其它传统浏览器功能的支持的技术是存在的。虽然实现起来并不容易,而且会增加成本,但是Ajax社区的成功需要将传统的浏览器功能构建到Ajax应用程序中。因此,我强烈呼吁Ajax开发人员构建支持这些功能的Ajax应用程序!
结束语
在本文中,我着重阐明了Ajax应用程序为什么需要遵从传统的Web交互方式并提供传统的Web可用功能。我确定可以通过创建在碎片标识符中包含客户端状态信息的“Ajax URI” ,从而将这些功能编程到Ajax应用程序中。
阅读相关代码,您会发现,由于状态处理代码通常非常重要,再加上不同浏览器之间常常不兼容,实现完整的通用解决方案是相当困难的。而Backbase Ajax引擎通过开箱即用地提供所需功能,为该问题提供了一种解决方案。