UML软件工程组织

结合 Ajax 进行 PHP 开发,第 2 部分: 后退、前进和刷新
Mike Brittain (mike@mikebrittain.com), 技术主管, ID Society   来源:IBM

异步 JavaScript 和 XML(Asynchronous JavaScript and XML,Ajax)驱动的 Web 站点的主要挑战之一是缺少后退按钮。“结合 Ajax 进行 PHP 开发” 系列包括两部分,第 1 部分创建了 Ajax 相册应用程序,这一部分我们将使用 JavaScript 为其建立一个历史堆栈(history stack)。这个堆栈以 Web 浏览器中的历史记录工具为蓝本,为该应用程序提供后退、前进和刷新按钮。

 简介

第 1 部分 介绍了如何用 Sajax、PHP 和 JavaScript 开发基本的相册。在为应用程序建立历史堆栈的过程中,我们将依靠客户端技术,并将其直接与第 1 部分的代码结合在一起。本文假设读者了解 JavaScript 和浏览器 cookie。

在浏览器中保存状态

在网上冲浪的时候,总是从一个页面到另一个页面,从一个站点到另一个站点。在这个过程中,Web 浏览器忠实地记录了您曾经到过何处的历史记录,创建了一条面包屑型(breadcrumbs)数字轨迹,沿着这条轨迹能够一步一步地回到出发点。后退按钮允许您回到上一个动作之前所在的位置,从这个意义上说它就是 Web 上的撤销按钮。

Web 是一种按页划分的的媒体。浏览器工具栏中的后退和前进按钮指引着浏览器从一个页面移动到另一个页面。当 Macromedia 的 Flash 风行一时的时候,开发人员和用户发现富互联网应用程序(Rich Internet Application,RIA)打破了这种模式。用户可以在几个站点上浏览,然后登录一个基于 Flash 的网站,在这个网站上消磨几分钟。当用户单击后退按钮时,游戏结束了。用户没有回到先前的那个 Flash 站点,完全不知道到了什么地方。

对于完全基于 Ajax 的网站 —— RIA 的另一种形式,情况也是如此。允许用户与一个页面进行多次交互的网站很容易受到后退按钮的困扰,或者受到任何历史记录按钮的困扰(就此而言)。前进和重载按钮的问题与后退按钮的问题一样。

Web 浏览器内置的内部历史记录机制是一个不可逃避的问题。出于安全的原因,开发人员不能篡改浏览器历史记录或者任何相关按钮。还有可用性的问题。设想一下,如果后退按钮突然弹出一个神秘的警告提示或者用户被打发到一个新的网站上去,用户该是多么困惑。

构建历史堆栈

虽然不能改变浏览器历史记录,但是可以自己构建一个在 RIA 中使用的历史记录。显然,它在某种程度上应该与浏览器的标准导航工具分开,但正如前面所说的,富应用程序在一定程度上背离了 Web 的页面到页面的标准模式。

我们将建立一个堆栈来管理应用程序的历史事件记录,也就是说存储一个列表,在表的最后添加元素。堆栈用于按照后进先出(LIFO)的顺序存储数据。虽然回退的时候并没有删除堆栈顶部的数据,但这个模型跟我们的需要非常接近。在 JavaScript 中,堆栈可以用数组来管理。

与堆栈在一起的还有一个指针,指示我们在堆栈中的当前位置。当我们在应用程序中单击的时候,新的事件将被压入堆栈顶部,指针指向最后添加的元素。单击应用程序的后退和前进按钮时,不会在堆栈中添加新的事件,而是移动堆栈的指针。

想一想使用后退按钮时历史堆栈中会发生什么:浏览器返回上一次查看的页面,原来不能用的前进按钮突然之间变得可用了。浏览新的页面时,前进按钮再次变成灰色。浏览器历史记录中较晚保存的元素将被弹出堆栈,新的事件被压入堆栈顶部。我们将在自己创建的历史堆栈中再现这种行为。

我们的目标是创建一组可用的历史记录按钮:后退、前进和刷新,如图 1 所示。

图 1. 后退、前进和刷新的历史记录按钮显示在左侧,不可用状态显示在右侧

 

可重用的设计

JavaScript 使用非常宽松的方法创建对象和类,但仍然能够建立可重用的代码。首先列出历史堆栈需要的功能,然后用 JavaScript 建立堆栈模型。在把历史堆栈集成到相册应用程序之前,首先要建立一个简单的页面来测试其功能。这样做有两方面的好处:测试页有助于将精力集中到开发和测试类的核心功能上,建立单独的测试页可以避免混淆历史堆栈和相册的功能,从而确保可重用性。

用 cookie 缓冲

我们需要应用程序的历史记录在整个浏览器会话中都存在。只要用户仍在查看相册页面,历史堆栈对象就会一直存在。每当发生更改的时候,这个类就会将整个历史记录复制到浏览器 cookie 中。如果用户在同一个浏览器会话中离开该页之后又返回,那么将返回他离开该应用程序时所在的同一个位置。

编写类

我们来看看历史堆栈中需要存储的数据或属性。前面已经讨论了堆栈(数组)和指针。stack_limit 属性可以防止因为数据过多而造成的 cookie 溢出(参见清单 1)。在实践中,我们希望在删除最老的记录之前能够存储 40-50 个事件。出于测试的目的,我们将该值设置为 15。

清单 1. 历史堆栈的构造,包括类的属性

function HistoryStack ()
{
this.stack = new Array();
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 ()
{
return this.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)
{
// Go back...
if (increment < 0) {
this.current = Math.max(0, this.current + increment);

// Go forward...
} else if (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)
{
var cookie_str = name + "=" + escape(value);
document.cookie = cookie_str;
};


HistoryStack.prototype.getCookie = function(name)
{
if (!name) return '';

var raw_cookies, tmp, i;
var cookies = new Array();

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) {
return cookies[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()
{
var tmp_stack = this.getCookie('CHStack');
if (tmp_stack != '') {
this.stack = tmp_stack.split(',');
}

var tmp_current = parseInt(this.getCookie('CHCurrent'));
if (tmp_current >= -1) {
this.current = tmp_current;
}
};

回首页

测试类

可以用简单的 HTML 页面和一些 JavaScript 来测试完成的类。测试页面将在上方显示历史记录按钮,只有活动的按钮是突出显示并且可以单击的。我们没有建立复杂的测试应用程序,该页面在每次单击链接时仅仅生成随机数。这些数字就是记录到历史堆栈中的事件。堆栈也在页面上显示,指针标记的当前记录用粗体显示。

清单 7. 测试历史堆栈的简单 HTML 页面

<html>
<head>
<title></title>
</head>

<body>

<div id="historybuttons"></div>
<div>
<a href="#" onclick="do_add(); return false;">Add Random
Resource</a>
</div>
<div id="output" style="margin-top:40px;"></div>

</body>
</html>

在该 HTML 页面的头部需要添加清单 8 所示的 JavaScript 代码。这段代码首先实例化一个新的历史堆栈对象,并载入可能已经保存到浏览器 cookie 中的所有数据。

我们定义了四个 do_*() 函数,这些事件处理程序将添加到后退、前进和刷新按钮的链接中,此外还有 Add Random Resource 链接,如清单 7 所示。

display() 函数检查历史记录对象的当前状态,并为历史记录按钮生成 HTML。它还生成历史记录中存储的项目列表。

 清单 8. 集成历史记录类和测试页面的 JavaScript

<script type="text/javascript" src="history.js"></script>
<script type="text/javascript">

var myHistory = new HistoryStack();
myHistory.load();

function do_add()
{
var num = Math.round(Math.random() * 1000);
myHistory.addResource(num);
display();
return false;
}

function do_back()
{
myHistory.go(-1);
display();
}

function do_forward()
{
myHistory.go(1);
display();
}

function do_reload()
{
myHistory.go(0);
}

function display()
{
// Display history buttons
var str = '';
if (myHistory.hasPrev()) {
str += '<a href="#" onclick="do_back(); return false;">'
+ '<img src="icons/back_on.gif" alt="Back"
/></a> ';
} else {
str += '<img src="icons/back_off.gif" alt="" /> ';
}
if (myHistory.hasNext()) {
str += '<a href="#" onclick="do_forward(); return
false;">'
+ '<img src="icons/forward_on.gif" alt="Forward" />'
+ '</a> ';
} else {
str += '<img src="icons/forward_off.gif" alt="" /> ';
}
str += '<a href="#" onclick="do_reload(); return false;">'
+ '<img src="icons/reload.gif" alt="Reload"
/></a>';
document.getElementById("historybuttons").innerHTML = str;

// Display the current history stack, highlighting the current
// position.
var str = '<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.onload = function () {
display();
};
</script>

运行该测试页面,可以看到历史记录按钮反映了历史堆栈的状态(见图 2)。比如,第一次加载页面时,按钮都是灰色的。向堆栈中添加一些记录后,后退按钮就变成活动的了。如果在堆栈中回退,前进按钮就变亮了。还要注意的是,如果单击几次后退然后再单击 Add,那么堆栈会被截掉一部分,新的事件 被压入缩短的堆栈顶部。

图 2. 历史堆栈的测试页面

 

 测试完该类后,就可以进入最激动人心的阶段了。
 
 回首页

集成历史记录对象和相册

我们将从第 1 部分留下的问题开始,直接从相册页面调用历史堆栈。不需要修改任何 PHP 文件。

首先需要添加一个 div 标记来存放历史记录按钮。如清单 7 所示。

<div id="historybuttons"></div>

历史堆栈代码被保存到一个 .js 文件中,该文件将链接到相册页面。

<script type="text/javascript" src="history.js"></script>

需要实例化历史堆栈对象并从缓冲区加载它们。这些操作可以添加到相册页面上已有脚本的前面。

var myHistory = new HistoryStack();
myHistory.load();

在针对历史堆栈的测试应用程序中,只存储随机数作为事件。我们可以在历史记录中存储需要的任何信息,但是要记住,当用户单击应用程序的后退按钮时,还要确定历史堆栈中的内容是什么。应用程序只有两个动作与 x_get_table() 和 x_get_image() 函数有关。因此对于每个表链接,可以存储名称 table 再加上 start 和 step 值作为事件标识符,比如 table-10-5。类似地,可以存储名称 image 和将被查看图像的 index,如 image-20。

在第 1 部分中,相册中的每个链接都是由 get_table_link() 和 get_image_link() 两个函数生成的。通过编辑这些函数,可以在调用 Sajax 函数之前让该函数先调用历史堆栈。清单 9 以粗体显示了这些变化。

 清单 9. get_table_link() 和 get_image_link() 函数的更新版本

function get_table_link ( $title, $start, $step ) {
$link = "myHistory.addResource('table-$start-$step'); "
."x_get_table($start, $step, to_window); "
."return false;";
return '<a href="#" onclick="' . $link . '">' . $title
.'</a>';
}
function get_image_link ( $title, $index ) {
$link = "myHistory.addResource('image-$index'); "
."x_get_image($index, to_window); "
."return false;";
return '<a href="#" onclick="' . $link . '">' . $title .
'</a>';
}

当应用程序进行 Sajax 调用时,to_window() 作为回调函数在页面上重新生成 HTML。在测试应用程序中,我们用函数 display()(清单 8)完成了两项任务:更新页面输出和更新历史记录按钮的状态。现在将在已有的 to_window() 函数体中添加下列函数调用:

display_history_buttons();

该函数的定义如清单 10 所示。

清单 10. display_history_buttons() 函数

function display_history_buttons()
{
var str = '';
if (myHistory.hasPrev()) {
str += '<a href="#" onclick="do_back(); return false;">
<img src="icons/back_on.gif" alt="Back" /></a>';
} else {
str += '<img src="icons/back_off.gif" alt="" />';
}
if (myHistory.hasNext()) {
str += '<a href="#" onclick="do_forward(); return false;">
<img src="icons/forward_on.gif" alt="Forward" /></a>';
} else {
str += '<img src="icons/forward_off.gif" alt="" />';
}
str += '<a href="#" onclick="do_reload(); return false;">
<img src="icons/reload.gif" alt="Reload" /></a>';
document.getElementById("historybuttons").innerHTML = str;
}

在开始跟踪相册应用程序的历史记录之前,只需要在页面加载过程中调用 x_get_table() 函数即可。这样就可以调用通过 Sajax 显示的初始表。

现在已经有了历史堆栈,但是我们不希望每次打开该应用程序时都要从头开始。相反,我们希望从离开的地方开始。因此需要添加 load_current() 函数以扩展应用程序,加载页面时会调用该函数。添加后退和前进按钮处理程序时,还将调用该函数,根据保存到历史堆栈中的事件 ID 来更新页面。

 清单 11. load_current() 函数

function load_current()
{
// No existing history.
if (myHistory.stack.length == 0) {
x_get_table(to_window);
myHistory.addResource('table-0-5');

// Load from history.
} else {
var current = myHistory.getCurrent();
var params = current.split('-');
if (params[0] == 'table') {
x_get_table(params[1], params[2], to_window);
} else if (params[0] == 'image') {
x_get_image(params[1], to_window);
}
}
}

onload 处理程序需要进行相应的修改:

window.onload = function () {
load_current();
};

最后,添加清单 12 中的历史记录按钮处理例程。注意处理程序和测试应用程序的相似性。

清单 12. 历史记录按钮事件处理程序

function do_back()
{
myHistory.go(-1);
load_current();
}

function do_forward()
{
myHistory.go(1);
load_current();
}

function do_reload()
{
myHistory.go(0);
}

至此就完成了历史堆栈到相册应用程序的集成。完成后的产品如图 3 所示。

图 3. 与相册应用程序结合的历史记录按钮

 

 打开应用程序并单击链接,就会看到存储在浏览器 cookie 中的历史堆栈和指针。
CHCurrent = 4
CHStack = table-0-5%2Cimage-1%2Cimage-2%2Cimage-3%2Ctable-3-5

如果正在运行 Mozilla Firefox 并下载了 Web Developer Toolbar 扩展,那么这些操作就很容易实现。

 回首页

结束语

我们介绍了如何创建一个自定义的历史堆栈来跟踪 Ajax 应用程序中的事件。可以在应用程序中添加 Web 浏览器上常见的后退、前进和刷新按钮来导航自定义的历史堆栈。

为解决这一难题,我们确定了问题所在,创建了能应用于其他应用程序的可重用解决方案。我们没有直接在相册应用程序中建立历史堆栈,而是用一个简单的页面测试这个类。这样做有助于建立不会严格绑定到某个应用程序的解决方案,该解决方案可用于其他 Ajax 应用程序来解决同样的问题。

 

版权所有:UML软件工程组织