简介
第1部分介绍了如何用Sajax、PHP和JavaScript开发基本的相册。在为应用程序建立历史堆栈的过程中,我们将依靠客户端技术,并将其直接与第1部分的代码结合在一起。本文假设读者了解JavaScript和浏览器cookie。
在浏览器中保存状态
在网上冲浪的时候,总是从一个页面到另一个页面,从一个站点到另一个站点。在这个过程中,Web浏览器忠实地记录了您曾经到过何处的历史记录,创建了一条面包屑型(breadcrumbs)数字轨迹,沿着这条轨迹能够一步一步地回到出发点。后退按钮允许您回到上一个动作之前所在的位置,从这个意义上说它就是Web上的撤销按钮。
Web是一种按页划分的的媒体。浏览器工具栏中的后退和前进按钮指引着浏览器从一个页面移动到另一个页面。当Macromedia的Flash风行一时的时候,开发人员和用户发现富互联网应用程序(RichInternetApplication,RIA)打破了这种模式。用户可以在几个站点上浏览,然后登录一个基于Flash的网站,在这个网站上消磨几分钟。当用户单击后退按钮时,游戏结束了。用户没有回到先前的那个Flash站点,完全不知道到了什么地方。
对于完全基于Ajax的网站——RIA的另一种形式,情况也是如此。允许用户与一个页面进行多次交互的网站很容易受到后退按钮的困扰,或者受到任何历史记录按钮的困扰(就此而言)。前进和重载按钮的问题与后退按钮的问题一样。Web浏览器内置的内部历史记录机制是一个不可逃避的问题。出于安全的原因,开发人员不能篡改浏览器历史记录或者任何相关按钮。还有可用性的问题。设想一下,如果后退按钮突然弹出一个神秘的警告提示或者用户被打发到一个新的网站上去,用户该是多么困惑。
构建历史堆栈
虽然不能改变浏览器历史记录,但是可以自己构建一个在RIA中使用的历史记录。显然,它在某种程度上应该与浏览器的标准导航工具分开,但正如前面所说的,富应用程序在一定程度上背离了Web的页面到页面的标准模式。
我们将建立一个堆栈来管理应用程序的历史事件记录,也就是说存储一个列表,在表的最后添加元素。堆栈用于按照后进先出(LIFO)的顺序存储数据。虽然回退的时候并没有删除堆栈顶部的数据,但这个模型跟我们的需要非常接近。在JavaScript中,堆栈可以用数组来管理。
与堆栈在一起的还有一个指针,指示我们在堆栈中的当前位置。当我们在应用程序中单击的时候,新的事件将被压入堆栈顶部,指针指向最后添加的元素。单击应用程序的后退和前进按钮时,不会在堆栈中添加新的事件,而是移动堆栈的指针。想一想使用后退按钮时历史堆栈中会发生什么:浏览器返回上一次查看的页面,原来不能用的前进按钮突然之间变得可用了。浏览新的页面时,前进按钮再次变成灰色。浏览器历史记录中较晚保存的元素将被弹出堆栈,新的事件被压入堆栈顶部。我们将在自己创建的历史堆栈中再现这种行为。
我们的目标是创建一组可用的历史记录按钮:后退、前进和刷新,如图1所示。
图1.后退、前进和刷新的历史记录按钮显示在左侧,不可用状态显示在右侧
#p#分页标题#e#
编写类
我们来看看历史堆栈中需要存储的数据或属性。前面已经讨论了堆栈(数组)和指针。stack_limit属性可以防止因为数据过多而造成的cookie溢出(参见清单1)。在实践中,我们希望在删除最老的记录之前能够存储40-50个事件。出于测试的目的,我们将该值设置为15。
清单1.历史堆栈的构造,包括类的属性
functionHistoryStack()
{
this.stack=newArray();
this.current=-1;
this.stack_limit=15;
}
除了这三个属性外,该类还需要一些方法来添加元素、检索堆栈数据以及将堆栈数据保存到浏览器cookie中。首先看一看addResource()方法,它用于将记录压入历史堆栈的堆栈顶部(参见清单2)。注意,如果堆栈的长度超过了stack_limit,那么最老的记录将从堆栈中移走。
清单2.addResource()方法,向历史堆栈的堆栈顶部添加记录
HistoryStack.prototype.addResource=function(resource)
{
if(this.stack.length>0){
this.stack=this.stack.slice(0,this.current+1);
}
this.stack.push(resource);
while(this.stack.length>this.stack_limit){
this.stack.shift();
}
this.current=this.stack.length-1;
this.save();
};
给历史堆栈添加的以下三个方法用于从该类中获取信息(参见清单3)。getCurrent()返回堆栈指针指向的当前记录,这在堆栈中导航的时候非常有用。hasPrev()和hasNext()方法返回Boolean值,告诉我们当前记录之前或之后是否还有记录,或者指示我们到达了堆栈顶部或堆栈尾部。这些方法很简单,但是确定后退和前进按钮的状态时很有用。
清单3.历史堆栈定义的方法
HistoryStack.prototype.addResource=function(resource)
HistoryStack.prototype.getCurrent=function()
{
returnthis.stack[this.current];
};
HistoryStack.prototype.hasPrev=function()
{
return(this.current>0);
};
HistoryStack.prototype.hasNext=function()
{
return(this.current<this.stack.length-1&&this.current>-1);
};
现在就可以向历史堆栈中添加记录并确定所在的位置了。但还是无法在堆栈中导航。清单4中定义的go()方法允许我们在堆栈中来回移动。通过传递正或负的增量就可以在堆栈中向前或向后移动。这与JavaScript内置的location.go()方法类似。既然模仿内置功能,为何不根据这些已有的方法建立模型呢?
此外,我们还可用该方法实现刷新功能。可以通过传递正或负的参数在堆栈中导航。传递零时则会刷新当前页面。
清单4.历史堆栈的go()方法
HistoryStack.prototype.go=function(increment)
{
//Goback...
if(increment<0){
this.current=Math.max(0,this.current+increment);
//Goforward...
}elseif(increment>0){
this.current=Math.min(this.stack.length-1,this.current+increment);
//Reload...
}else{
location.reload();
}
this.save();
};
到目前为止,只要HistoryStack对象存在于当前文档中,这个新建的类就能正常工作。我们已经讨论了刷新页面会造成数据丢失的问题,现在来解决它。清单5中添加了在浏览器cookie中设置和访问数据的方法。所要做的只是设置每个cookie的名称值对。因为只需要在浏览器会话中保存cookie,而不需要设置有效期。为了简化示例,我们不考虑其他参数,如secure、domain和path。
注意:如果该类需要对cookie做复杂处理,更明智的办法是使用完全独立的cookie管理类。建立和读取cookie有点偏离历史堆栈的正题。如果JavaScript允许指定方法和属性访问的作用域,也可以将这些方法设成私有的。
清单5.建立和访问浏览器cookie的方法
HistoryStack.prototype.setCookie=function(name,value)
{
varcookie_str=name+"="+escape(value);
document.cookie=cookie_str;
};
HistoryStack.prototype.getCookie=function(name)
{
if(!name)return'';
varraw_cookies,tmp,i;
varcookies=newArray();
raw_cookies=document.cookie.split(';');
for(i=0;i<raw_cookies.length;i++){
tmp=raw_cookies[i].split('=');
cookies[tmp[0]]=unescape(tmp[1]);
}
if(cookies[name]!=null){
returncookies[name];
}else{
return'';
}
};
定义了管理任何cookie的方法之后,可以编写另外两个类专门处理历史堆栈的类。save()方法将堆栈转化成字符串并保存到cookie中,load()重新将字符串解析成用于管理历史堆栈的数组(参见清单6)。
清单6.save()和load()方法
HistoryStack.prototype.save=function()
{
this.setCookie('CHStack',this.stack.toString());
this.setCookie('CHCurrent',this.current);
};
HistoryStack.prototype.load=function()
{
vartmp_stack=this.getCookie('CHStack');
if(tmp_stack!=''){
this.stack=tmp_stack.split(',');
}
vartmp_current=parseInt(this.getCookie('CHCurrent'));
if(tmp_current>=-1){
this.current=tmp_current;
}
};
#p#分页标题#e#
测试类
可以用简单的HTML页面和一些JavaScript来测试完成的类。测试页面将在上方显示历史记录按钮,只有活动的按钮是突出显示并且可以单击的。我们没有建立复杂的测试应用程序,该页面在每次单击链接时仅仅生成随机数。这些数字就是记录到历史堆栈中的事件。堆栈也在页面上显示,指针标记的当前记录用粗体显示。
清单7.测试历史堆栈的简单HTML页面
<html>
<head>
<title></title>
</head>
<body>
<divid="historybuttons"></div>
<div>
<ahref="#">AddRandom
Resource</a>
</div>
<divid="output"style="margin-top:40px;"></div>
</body>
</html>
在该HTML页面的头部需要添加清单8所示的JavaScript代码。这段代码首先实例化一个新的历史堆栈对象,并载入可能已经保存到浏览器cookie中的所有数据。
我们定义了四个do_*()函数,这些事件处理程序将添加到后退、前进和刷新按钮的链接中,此外还有AddRandomResource链接,如清单7所示。
display()函数检查历史记录对象的当前状态,并为历史记录按钮生成HTML。它还生成历史记录中存储的项目列表。
清单8.集成历史记录类和测试页面的JavaScript
<scripttype="text/javascript"src="history.js"></script>
<scripttype="text/javascript">
varmyHistory=newHistoryStack();
myHistory.load();
functiondo_add()
{
varnum=Math.round(Math.random()*1000);
myHistory.addResource(num);
display();
returnfalse;
}
functiondo_back()
{
myHistory.go(-1);
display();
}
functiondo_forward()
{
myHistory.go(1);
display();
}
functiondo_reload()
{
myHistory.go(0);
}
functiondisplay()
{
//Displayhistorybuttons
varstr='';
if(myHistory.hasPrev()){
str+='<ahref="#">'
+'<imgsrc="icons/back_on.gif"alt="Back"
/></a>';
}else{
str+='<imgsrc="icons/back_off.gif"alt=""/>';
}
if(myHistory.hasNext()){
str+='<ahref="#">'
+'<imgsrc="icons/forward_on.gif"alt="Forward"/>'
+'</a>';
}else{
str+='<imgsrc="icons/forward_off.gif"alt=""/>';
}
str+='<ahref="#">'
+'<imgsrc="icons/reload.gif"alt="Reload"
/></a>';
document.getElementById("historybuttons").innerHTML=str;
//Displaythecurrenthistorystack,highlightingthecurrent
//position.
varstr='<div>History:</div>';
for(i=0;i<myHistory.stack.length;i++){
if(i==myHistory.current){
str+='<div><b>'+myHistory.stack[i]+
'</b></div>';
}else{
str+='<div>'+myHistory.stack[i]+'</div>';
}
}
document.getElementById("output").innerHTML=str;
}
window.οnlοad=function(){
display();
};
</script>
运行该测试页面,可以看到历史记录按钮反映了历史堆栈的状态(见图2)。比如,第一次加载页面时,按钮都是灰色的。向堆栈中添加一些记录后,后退按钮就变成活动的了。如果在堆栈中回退,前进按钮就变亮了。还要注意的是,如果单击几次后退然后再单击Add,那么堆栈会被截掉一部分,新的事件被压入缩短的堆栈顶部。
图2.历史堆栈的测试页面#p#分页标题#e#
在第1部分中,相册中的每个链接都是由get_table_link()和get_image_link()两个函数生成的。通过编辑这些函数,可以在调用Sajax函数之前让该函数先调用历史堆栈。清单9以粗体显示了这些变化。
清单9.get_table_link()和get_image_link()函数的更新版本
functionget_table_link($title,$start,$step){
$link="myHistory.addResource('table-$start-$step');"
."x_get_table($start,$step,to_window);"
."returnfalse;";
return'<ahref="#">'.$title.'</a>';
}
functionget_image_link($title,$index){
$link="myHistory.addResource('image-$index');"
."x_get_image($index,to_window);"
."returnfalse;";
return'<ahref="#">'.$title.'</a>';
}
当应用程序进行Sajax调用时,to_window()作为回调函数在页面上重新生成HTML。在测试应用程序中,我们用函数display()(清单8)完成了两项任务:更新页面输出和更新历史记录按钮的状态。现在将在已有的to_window()函数体中添加下列函数调用:
display_history_buttons();
该函数的定义如清单10所示。
清单10.display_history_buttons()函数
functiondisplay_history_buttons()
{
varstr='';
if(myHistory.hasPrev()){
str+='<ahref="#">
<imgsrc="icons/back_on.gif"alt="Back"/></a>';
}else{
str+='<imgsrc="icons/back_off.gif"alt=""/>';
}
if(myHistory.hasNext()){
str+='<ahref="#">
<imgsrc="icons/forward_on.gif"alt="Forward"/></a>';
}else{
str+='<imgsrc="icons/forward_off.gif"alt=""/>';
}
str+='<ahref="#">
<imgsrc="icons/reload.gif"alt="Reload"/></a>';
document.getElementById("historybuttons").innerHTML=str;
}
在开始跟踪相册应用程序的历史记录之前,只需要在页面加载过程中调用x_get_table()函数即可。这样就可以调用通过Sajax显示的初始表。
现在已经有了历史堆栈,但是我们不希望每次打开该应用程序时都要从头开始。相反,我们希望从离开的地方开始。因此需要添加load_current()函数以扩展应用程序,加载页面时会调用该函数。添加后退和前进按钮处理程序时,还将调用该函数,根据保存到历史堆栈中的事件ID来更新页面。
清单11.load_current()函数
functionload_current()
{
//Noexistinghistory.
if(myHistory.stack.length==0){
x_get_table(to_window);
myHistory.addResource('table-0-5');
//Loadfromhistory.
}else{
varcurrent=myHistory.getCurrent();
varparams=current.split('-');
if(params[0]=='table'){
x_get_table(params[1],params[2],to_window);
}elseif(params[0]=='image'){
x_get_image(params[1],to_window);
}
}
}
onload处理程序需要进行相应的修改:
window.οnlοad=function(){
load_current();
};
最后,添加清单12中的历史记录按钮处理例程。注意处理程序和测试应用程序的相似性。
清单12.历史记录按钮事件处理程序
functiondo_back()
{
myHistory.go(-1);
load_current();
}
functiondo_forward()
{
myHistory.go(1);
load_current();
}
functiondo_reload()
{
myHistory.go(0);
}
至此就完成了历史堆栈到相册应用程序的集成。完成后的产品如图3所示。
图3.与相册应用程序结合的历史记录按钮