AJAX开发支持标准浏览器按钮功能的方法
摘要
AJAX 应用因为它们的表现力的丰富、更加互动和更加迅速的响应得到了赞扬声;这些优点都是通过使用XMLHttpRequest对象来动态的载入数据而获得的, 而不是重新载入新的页面。在大量的宣传和刺激中,却有一些批评的声音指出,AJAX应用破坏了一些重要的浏览器特性,这其中包括对前进/后退按钮的支持。
本文首先解释了为什么除非明确的将那些功能做进AJAX应用,否则前进/后退按钮和其他一些浏览器功能不能正常工作。然后简单的列出开发者如何解决上述问题,最后我们将详细的看一看Backbase AJAX引擎是如何提供对前进/后退按钮和其他一些浏览器功能的支持的。
AJAX应用需要一个后退按钮吗?
AJAX许诺允许开发者仅仅使用标准的浏览器技术开发有更好用户体验的和高度互交的WEB应用,这种技术常常是指DHTML。
以前,开发者常常不得不在rich和reach之间做出选择;前者是指具有高度互交性的交互式的用户接口,后者是指一个运行在所有WEB浏览器上的、不用额外的安装机制的前台终端。AJAX应用将使得前台终端既“rich”又“reach”。
但是到底什么是真正的一个界面“rich”的含义呢?而什么又是一个应用“reach”的含义呢?
“rich” 的概念不容易定义,但是容易被直观的感受。如果你看到一个“rich”界面,你将明白什么是“rich”界面。桌面应用像微软的Office就是一个 “rich”的界面。一个“rich”的界面使用先进的UI控制技术,如Tabs和上下文菜单。它们提供前进的互交手段,像当它们获得焦点的时候,UI元 素的drag-and-drop和highlighting。传统的浏览器应用不是“rich”的。它们被限制在一些简单的控制器里,像Form,而交互 只是简单的依靠点击链接到一个新的页面。看看微软的email客户端就能看出其中的差别:Outlook是“rich”的,而hotmail则不是。
AJAX应用因为它们的富有的表现力而得到表扬,Google的Gmail就是一个经常提及的例子。其他的Google所做的AJAX应用,如Google Suggest和Google Map。微软即将发布的Web mail客户端,代号为:“Kahuna”,或者Backbase RSS Reader也包含了高级的控制器和互交的模型。看一看Dan Grossman的列表:Top 10 Ajax Applications,那里是一些激动人心的关于“rich”界面的列表。
因此我可以得出结论,AJAX应用很明白的满足“rich”的概念。但是它是否满足“reach”呢?
在AJAX应用的绝大多数基本表单里,如果界面运行在一个WEB浏览器里,那么这个应用是“reach”的。AJAX应用是基于标准的浏览器的,因此能够被一个浏览器所访问。
但是,仅仅被浏览器所理解还是不够的。Jakob Nielson在他的文章Flash: 99% Bad里,指出Flash:“破坏了WEB的基本的交互风格”。在使用WEB应用的时候,终端用户期望一个确定的交互风格。应用需要提供传统的WEB交互风格,并且提供如下的可用性特性:
。必须提供“后退/前进”按钮,用户能够浏览网页历史纪录
。用户能够使用书签
。必须提供Deep links,以便用户能够将其通过email发送给朋友或同事
。必须提供“刷新”按钮,用来刷新当前状态,而不是通过重新初始化应用来获得
。开发人员可以通过使用“view source”来查看源代码
。终端用户能够使用“find”来搜索页面
。搜索引擎能够对网页编索引,能够产生一个Deep link来搜索它们
看一看10个顶级的AJAX应用,表明大多数的当前AJAX应用的确破坏了WEB的基本交互风格。在接下来的部分里,我们将看一看为什么很多的AJAX应用会这样。
为什么AJAX应用经常性的破坏了后退按钮?
我们今天所知道的WEB牢固的建立在以下三个原则的基础上:
。使用(D)HTML建立的界面
。使用HTTP为客户/服务器通讯
。使用URIs寻址
上面的原则决定了我们所获取的WEB应用有一定的局限性,而AJAX应用正是通过冲破这种局限性的限制而使得界面变得更加丰富。正像我以前的文章:A Backbase Ajax Front-end for J2EE Applications所 解释的。AJAX引入了广泛使用的Javascript(AJAX中的J)来创建丰富的UI界面控制器和交互。AJAX也引入了异步的XML通讯 (AJAX中的A和X),这是通过引入XMLHttpRequest对象来载入新的数据和表现层逻辑,而没有页面刷新。然而,当前的AJAX模型,不能够 定位如何处理URIs。
作为改变使用(D)HTML和HTTP的一个直接后果,AJAX应用破坏了作为WEB基本交互风格的后退按钮和其他的元素。在本文余下的部分,我将解释如何在AJAX方式下通过处理URIs来修补这种破坏。我首先将说一说为什么URIs会和传统的WEB应用的交互有关系。
用户交互用术语来说就是用户界面的状态发生了改变。终端用户发起了状态改变,浏览器客户端通过发送页面请求到服务器的方式来处理状态变化(REST原则),服务器通过发送一个新的页面和新的URIs到客户端来产生一个新的界面状态。
简而言之,每一个用户交互是这样别处理的,通过服务器的循环产生如下的结果:
。产生一个新的页面
。产生一个新的URI
因 为浏览器记录了连续的URIs到它的历史堆栈里,并且将当前的URI显示在地址栏里,WEB的可用性特性就被激活。在地址栏里,用户可以拷贝URI,并且 可以把它发送给朋友。当用户点击返回按钮或从email里粘贴一个URI到地址栏里,一个到服务器的循环就被触发。由于服务器负责状态管理,那么服务器就 能产生相应的页面。
AJAX应用和传统的WEB应用的最大的不同在于AJAX应用能够不通过页面的重载而处理用户的交互。通过XMLHttpRequest对象从服务器载入数据就是一个例子。使用Javascript处理客户端drag-and-drop是另外的一个例子。
在上面的两个例子中,状态的变化不是通过产生一个新的URI得到的。因此,点击后退按钮或者刷新按钮不能得到想要的效果,并且在地址栏里没有deep-link的URI。
为了提供传统的WEB可用性特性,AJAX应用需要像传统的WEB应用一样来处理URIs客户端。因此AJAX应用需要做如下的事:
。当一个客户端状态变化时,产生一个URI,并且发送到客户端
。当一个新的URI被客户端请求时,保存状态
做到了上面的两点,浏览器的历史纪录将会工作,浏览器的地址栏将显示一个能发送给朋友的URI。
另 外一个困难是决定什么时候AJAX引擎应该做上面的那两件事(例如,什么样的状态变化将导致产生一个新的URI)。在传统的模式中,每一个页面刷新都导致 一个URI的更新。在AJAX模式中,每一个客户端事件都可能产生一个URI到浏览器堆栈。交互设计师和开发者将决定什么样的状态变化是有意义的。因而只 对那些有意义的状态变化产生新的URI。
我将总结AJAX应用需要提供WEB可用性特性的客户端需求:
1. 产生历史纪录
a. 保存有意义的状态
b. 产生相应的URI
c. 将URI推到浏览器堆栈
2. 恢复历史纪录
a. 探测URI变化
b. 维护从URI得到的状态
在AJAX中支持后退按钮的基本设计
在这个部分,我们将讨论在AJAX应用中支持后退按钮的基本步骤。我将引入一些简单的代码来展示必需的步骤。关于如何实现跨浏览器兼容的后退按钮的支持的更为完整的方法已经实现了。在我喜欢的MikeStenhouse(Content with Style)和Brad Neuberg (OnJava)所写的两篇文章里有描述。
如图1所示,我的简单示例应用上有一个选择框,里面有两个选择项:Year 1和Year 2。因为本文的主旨,当选择框的值发生变化时,我们将纪录它的历史纪录。这意味着用户如果首先选择Year 2的话,然后点击后退按钮将退回到前面的选择。
图1:一个只有一个选择框的简单示例
这个示例应用的开始部分只是一些简单的HTML代码,里面有一些为选择框的值所作的getter和setter方法的JS代码:
<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>
我们首先是处理第一个需求:产生状态的历史纪录。就像前面所提到的,这个需求包括下面的三个部分:
1. 生历史纪录
a.保存有意义的状态
b.产生相应的URI
c.将URI推到浏览器堆栈
我希望保存的状态是每一次选择框的值的改变,因此我需要产生一个新的包含选择框的状态信息的URI。
为了保持对标准的因特网的适应,我使用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所示,每一次更新选择框的值的时候,浏览器地址栏得到了更新。请注意:对于IE来说,需要使用一个隐藏的frame才能获得同样的效果。同样,你可以到Mike Stenhouse或者Brad Neuberg的文章里完整的详细的内容。
图2:状态改变时历史堆栈得到更新
现在当选择框的值发生变化时,你拥有了一个能产生新的URI的事件处理。新的URI使用片断标识符来存储信息以维护之前的状态。有了它,我们再来看第二个需求:
2.恢复历史纪录
a.探测URI变化
b.维护从URI得到的状态
在步骤1里,我们通过
window.location.hash()方法更新了客户端的URI。这种调用没有导致服务器循环调用和页面刷新。我需要以一种AJAX的方式来处理客户端URI。
首先,我添加一个polling方法来检测浏览器历史里的URI。然后在页面的onload()事件里注册
pollHash()方法,然后每一千个毫秒运行一次。
Polling方法将调用
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使用一个隐藏的iframe
。更多的硬URIs(例子仅仅运行少于10个选择项的选择框)
。在创建的时候注册初始状态
完全处理在一个好的鲁棒性的要求下、兼容所有的浏览器的所有的传统WEB可用性特性并不容易实现。一个可以替代的方法是使用内嵌的AJAX工具箱来支持这些特性。
在下面的部分里,我将描述如何使用Backbase AJAX引擎来提供这些特性。我参考了Ajax forum on the Backbase DevNet来说明的。
案例研究:拥有后退按钮和Deep Link的AJAX论坛
Backbase AJAX引擎是一个成熟的、富WEB传统特性的AJAX软件包。强烈支持传统的WEB特性是Backbase的一个特点。Backbase DevNet包含了适合开发者的Backbase和AJAX信息。DevNet也有一个开发者的论坛。
Backbase Web包含了DevNet和一个用来讨论如何使用Backbase的论坛。为了展示这个论坛的“rich”和“reach”特性,我将一步步来讲述这个论坛的典型用法:
1. 一个开发者浏览不同主题的论坛阅读线程
2. 开发者拷贝线程的URI,并且粘贴到一个email中去。朋友将URI从email中拷贝到浏览器,并且打开相同的论坛线
3. 开发者点击后退按钮去浏览前一个线程
几个用户互交后的论坛界面的状态
当用户浏览“BXML”论坛,并且选择发送“Issue with vertical and horizontal menus”的时候,我们看一看论坛界面的状态和地址栏相应的URI。
论 坛和当前选择被选择上并且得到焦点。请求的线程被显示来阅读。URI包含了所有的相应的片断标识符的信息。在hash后面,我们看到了用来记录书签和 deep linking的完整的状态。“forum”的意思是开发者正在浏览WEB地址的论坛部分,“forum=2”的意思是BXML论坛被选择了, “thread=211”记录了当前选择的线程,最后,中括号的信息:“[5]”的意思是处理多个后退前进和书签的步骤。
图3:带AJAX URI的论坛状态(点击图片来察看其坊大图)
如果你访问Backbase论坛,你将会看到当每一个状态变化时,URI是如何更新的,甚至被客户端处理的URI和通过XMLHttpRequest对象的部分页面的更新的URI。
被其他浏览器窗口维护的论坛界面状态
现 在让我们看一看,当开发者发送当前的URI给朋友的时候,将要发生什么。朋友在浏览器窗口打开URI,并且希望看到相同的界面状态。在一个新的浏览器里, 这个状态需要重新创建。在我们的案例学习中,我通过从Firefox窗口拷贝URI到刚刚打开的IE窗口的方法模拟了这个需求。
在 浏览器的地址栏里输入URI马上就能产生一个服务端的请求。使用hash以前的部分,Backbase.com被载入进来,并且在这个过程中, Backbase AJAX引擎被初始化了。这个活跃的Backbase引擎讲读取URI的hash后面的部分。从那些信息里,Backbase引擎产生相应的状态,如进入 “forum”部分,并且设置论坛的BXML (id=2)和线程211。通过从服务器载入额外的内容并且更新客户端的部分接口,我们完成了上述的功能,并且没有页面更新。
为了后续的浏览器功能的处理,一个新的URI将被推入到浏览器的历史纪录。这个新的RUI能够在地址栏里看到,并且能够被用来做deep linking。“[0]”的意思是使用使用后退按钮的话,将没有在这个状态之前的状态可以返回。
图4:新的浏览器窗口维护的论坛状态(点击看放大图)
当用户点击后退按钮后的论坛界面状态
研究这个案例的第一步是通过用户交互产生一个带新的URI的界面状态的改变。这样,你将看到:一个新的URI被请求并且相应的状态被维护。
通 过点击后退按钮,用户将浏览他们之前阅读过的页面。浏览器是通过从历史纪录堆栈里找回前面的URI来处理这个请求的。Backbase AJAX引擎侦测到了这个变化,从历史纪录中读取先前的URI并且维护相应的状态:进入“forum”部分,选择论坛的"BXML" (id=2),并且线程为192。一个和上面的URI有着相同的句法的URI将在地址栏里被看到。
到此将结束本案例的学习。
图5:使用了后退按钮的论坛状态(点击看放大图)
AJAX应用的确需要后退按钮
在 过去的一些年里,我们开发人员选择开发这样的WEB应用界面:市场要求“reach”,而且愿意接受更少的甚至没有“rich”。但是,当前在AJAX方 面的兴奋点清楚地证明了:这种意愿实际上仅仅是暂时的。市场现在着重的要求了WEB应用需要和桌面应用相同的富表现力、交互和响应速度。
同时,终端用户已经习惯了传统的WEB交互风格。使用相同的模式和所有的WEB界面交互能够提高生产力。终端用户希望使用后退前进按钮,能够产生一个书签、deep link和能够使用刷新按钮,使用“find”来搜索整个页面,并且搜索引擎能够对AJAX应用作索引。
AJAX 社区接受了如下的观点:在AJAX应用中提供对于后退前进按钮和其他传统的浏览器特性的支持,在技术上是可以做到的,就像本文所谈到的那样。虽然使用和传 统的WEB开发一样的资源,这些特性不容易被实现。但是这个社区的成功将要求将传统的WEB特性加入进AJAX应用。因此,我强烈建议AJAX的开发者在 开发AJAX应用的时候支持这些特性。
结论
在这篇文章里,我主要讲述了为什么AJAX应用需要照做传统的WEB交互风格,提供对传统的WEB可用性特性的支持。我创建了一个应用:在这个应用里,通过创建在片断标识符里包含客户端状态信息的AJAX URI,我将这些特性写进了AJAX应用中。
通过上面的代码,你可以看到,一个完整的和一般的解决方案是很难得到的。给出一般的状态处理代码是不容易的。原因是跨浏览器的不兼容。Backbase AJAX引擎通过提供外部的需求功能来提供了这个问题的解决方案。
参考文献
- An Introduction to Ajax by David Teare (dev2dev, August 2005)
- Backbase RSS Reader
- Top 10 Ajax Applications by Dan Grossman (A Venture Forth, September 2005)
- Flash: 99% Bad, by Jakob Nielsen (Jakob Nielsen's Alertbox, October 2000)
- A Backbase Ajax Front-end for J2EE Applications by Mark Schiefelbein (dev2dev, August 2005)
- Fixing the Back Button and Enabling Bookmarking for AJAX Apps by Mike Stenhouse (Content with Style, June 2005)
- AJAX: How to Handle Bookmarks and Back Buttons by Brad Neuberg (OnJava, October 2005).
- Uniform Resource Identifier (URI): Generic Syntax (IETF RFC 3986, January 2005)
- Ajax forum on the Backbase DevNet
关于作者
Mark Schiefelbein自从2005年1月以来,担任Backbase公司的产品管理的主管。他组织了Backbase公司富网络应用技术的全球产品展览。
原文链接
http://dev2dev.bea.com/pub/a/2006/01/ajax-back-button.html