级别: 中级
Brett McLaughlin (), 作家兼编辑, O'Reilly Media, Inc.
2008 年 5 月 14 日
后端处理 — 服务器端脚本和程序 — 并不总能一投入到 Ajax 应用程序中就很好地运转。相反,若能仔细地提前规划以确保数据以恰当及有效的格式发送,反而能让整个应用程序更内聚,并能减少不必要的复杂性。在本文中,Brett McLaughlin 展示了一个好的服务器端脚本如何能补足 Ajax 行为。
在本系列的 第 1 部分,我们在 Nathan Smith 的 Hoverbox 代码(到初始 Hoverbox 代码的相关链接可以在 参考资料 部分找到)的基础上构建了一个视觉上异常丰富的前端来展示图片库。通过这个处理过程,您了解了一些基本的 UI 原则:人眼遵循的 Z-模式、保持页面一致性、如何减小从页面滑下的框中文本字体的大小以及 serif 和 sans-serif 字体的可读性间的差异。
完成第 1 部分的操作之后,您应该有了一个很像样的图片库,如图 1 所示,这就是我们在第 1 部分中完成的那个 Hoverbox UI:
在本篇文章中,我们将添加关键的后端处理以及一些特定于 Ajax 的代码以将图片库连到后端。
正如我先前提到的,Ajax 的大部分工作都是进行管道处理:设置好 XmlHttpRequest 对象、调用服务器端程序并获得响应。程序可能会改变,有关请求的细节可能不同,但不管在何种情况下,基本的设置都是相同的:
- 创建新的请求对象(最好是以跨浏览器的方式)。
- 构造一个请求 URL,可能还要 POST 请求数据。
- 设置一个方法,当从服务器收到响应时,用此方法为浏览器进行回调。
- 发送请求。
- 构造一个回调,此回调可以运行和执行 程序想要对服务器数据进行的任何操作。
更多细节可以在其他文章中(参见 参考资料 部分)找到,所以我们这里只给出一个大概。
首先,我们需要一个类似 XmlHttpRequest 的响应对象。让我们先来以独立于浏览器的方式创建一个。清单 1 是所需的代码:
/* Create a new XMLHttpRequest object to talk to the Web server */
var request = false;
/*@cc_on @*/
/*@if (@_jscript_version >= 5)
try {
request = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
try {
request = new ActiveXObject("Microsoft.XMLHTTP");
} catch (e2) {
request = false;
}
}
@end @*/
if (!request && typeof XMLHttpRequest != 'undefined') {
request = new XMLHttpRequest();
} |
我将上述代码保存到了名为 hoverbox-ajax.js 的文件中,我们将用该文件保存所有用于 hoverbox Web 应用程序的 JavaScript 代码。然后,需要在 HTML 的 head 元素中引用此文件:
现在,我们就可以实际连接到服务器端脚本并获得有关每个图像的一些信息。但该脚本可以接受哪些输入呢?输出又是什么格式呢?让我们试着让事情更趋于真实一些:假设您对服务器端代码没有完全的控制权。也可能获取数据的脚本已经写好,目前在做的只是构建一个新的支持 Ajax 的前端。或许您无需处理 PHP — 这类事由他人负责。不管怎样,您都需要知道脚本的一些规范以及该如何与其通信。
如下是应用程序需要与之通信的脚本的一个简要规范:
|
构建到 XmlHttpRequest 对象中的安全性机制之一是沙箱处理。简单地讲,这意味着运行在给定域(比如 newInstance.com)的 JavaScript 代码只能对同一域中的脚本发出请求。所以如果某脚本运行在本地机器上,那么它只能请求这个本地机器上(可能是本地机器上运行的 Web 服务器,比如 Apache 或 IIS)运行的其他脚本。
就本文而言,这意味着您的 Web 页面上的 JavaScript 不能调用 newInstance.com 上的代码,原因是您的页面没有在我的 Web 服务器上使用服务。您需要根据自己的具体设置调整本文中所提及的 URL。您可能还会想要简化服务器端脚本(在后续几个章节中会详细介绍)以便在没有对数据库的访问权限的时候,不会用到该数据库。 | |
- 脚本保存于特定的 URL。在文本,我们使用 。
- 脚本只接受单一输入:所需查看的图像名称。名称不应该有路径,也不应该有扩展名。所以对于像 img/photo07.jpg 这样的引用,脚本想要的只是 “photo7”。
- 脚本会返回图像名、标题、数据和描述,均用管道符分隔。
通常,您具有足够的信息可以让您返回到 JavaScript。但如果那样的话,您就没有机会尝试实际的脚本了,所以让我们稍微绕下路,实际构建一下规范所描述的 PHP 脚本(和 MySQL 数据库)。这样,您就能够实际在自己的机器上运行此应用程序了。
在设计应用程序时 — 尤其是可以在其中编写 JavaScript 的 Web 应用程序 — 您通常都不能决定您与之交互的数据库以及您连接到的服务器该如何运转。更多的时候是要遵循给定的规范或是一组示例输入所指定的格式。
为了能在一定程度上说明上述情况,我们将会构建一个简单的 PHP 脚本,而且不过多地考虑 Web 表单该如何设计。考虑如下的一个实际例子:有人将一个脚本交由您处理或至少给出了该脚本如何工作的细节。输入不多,但没关系,您是 Web 方面的专业人士,您应该知道如何处理最奇异的数据格式,对么?
由于这实际上更多涉及的是客户端,而且您也不过刚刚得到服务器端的规范,所以清单 2 给出了针对此服务器的 PHP。您既可以对其进行深入研读也可以完全忽略它。但要确保它与 Web 页面运行于同一服务器上,以便可以编写 JavaScript 来访问它。
上述代码会获得请求参数以及图像名称、在数据库中查找它并返回所有有关此图像的信息(由管道符 | 分隔),正如规范所规定的一样。
要让此过程更加趋于现实(尽管设置上有些复杂),让我们先来创建一个 SQL 数据库以存储每个图像的信息。如下所示的是来自我所使用的表的 phpMyAdmin 的一个屏幕快照(参见图 2):
这些列都非常简单:
- name:图像名称,应该与我们在 HTML 中的每个
img 元素的 src 属性中所使用的文件名相匹配。注意当我们要将所有这些连起来的时候,我们要去除 img/ 前缀,即图像所在的目录名,所以如果到某个图像的引用是 img/image_01.jpg,我们处理后的结果应该为 "image_01.jpg"。
- title:图像的标题,比如 “Sunrise Over the Bay”。该数据显示为用于每个图像的 Web 页右端最大的标题。
- date:也很容易,是图像拍摄的日期(顺便说一句,该日期并不一定是图像存储在数据库中的日期)。
- description:与图像相关的较长描述,我们要为每个图像显示的主要内容。
很显然,这是一种非常基础的表设计;虽然不怎么复杂,但主旨俱在。使用图像名称作为键不是一种很好的做法,而且还会引发各种各样的麻烦。但,这是数据库设计方面的问题,不在本文的讨论范围之内;这里的关键是设置一个从 Web 页到 JavaScript 到服务器端再到数据库的信息流,而这经常都是在处理实际的应用程序所要涉及的事情。
|
我曾在此数据库中使用过 BLOB 类型,但在导出时,结果得到的却是二进制数据 — 这真有点无法理解。所以不要被 SQL 数据文件中的这种怪异的格式化所吓倒。 | |
接下来,应该向表中插入示例数据。我在本文的 参考资料 部分提供了一个可下载的 SQL 批文件,它会创建表并插入全部五行数据。您可以针对自己的数据库使用这些数据,自然,花一下午时间为不是自己拍摄的照片键入 20 个描述不是什么愉快的事情。示例包括了 5 行,您可以添加更多的示例数据来填充剩余的图片库。
表中有了这些数据之后,就可以实际将前端(Web 表单)和后端(数据库)连接起来了。
现在,页面已经有了一个请求对象,称为 request ,并且用来从中获取数据的 PHP 脚本和数据库也准备好了。这时要做的就是构建一个向脚本发起请求的 JavaScript 函数,然后再将图片库中的图像连接到该函数。
在开始处理客户到服务器的连接前,需要首先确保图像能调用函数。所以让我们从一个简单的占位符 JavaScript 函数开始:
function getImageDetails() {
alert("Image details would get pulled here.");
}
|
我将上述代码放到了 hoverbox-ajax.js 中用于创建请求对象的代码的下面。此函数真的不过是一个占位符,但我们现在可以只调用它,而先不用顾及服务器端和 Ajax 细节。它先是创建一个请求对象,然后再弹出一个信息消息框。
现在,需要针对图像修改 HTML。先来看一下图片库中的每个图像是如何表示的:
乍看上去,这让人有些费解,我们不妨分开来看看。首先,这里有一个 a 标记,它不链接到任何东西(使用 # 符号作为对象)。当鼠标在该图像上停留的时候,它能让 CSS 规则就绪。然后,就是 img 标记。这是图片库默认显示的图像 — 较小版。最后,是第二个 img ,带 “preview” 类。如下所示的是该类的 CSS(取自 css/hoverbox.css):
.hoverbox a
{
cursor: default;
}
.hoverbox a .preview
{
display: none;
}
.hoverbox a:hover .preview
{
display: block;
position: absolute;
top: -33px;
left: -45px;
z-index: 1;
}
|
默认地,上述代码表明第二个图像 — 即图片库图像未缩小的较大版本 — 被隐藏(display 属性设为 none)。所以当页面加载时,由第二个图像标记所引用的图像不可见。
但是,当鼠标在其上悬停时,.preview 规则生效。该图像被显示在第一个图像的上方(原因是第二个图像的 z-index 被设为 1,一个很低的值),可以稍微向上移动一点以使该图像完全覆盖下面的图像。效果如图 3 所示:
我们需要的动作是当用户的鼠标移到该图像之上时调用我们的 JavaScript 处理程序。这意味着对每个 img 标记上都应该有如下处理:
将上述代码添加到这 20 个图像标记并重新加载页面。请您也尝试重新加载您的页面并查看结果。您觉得如何?图 4 显示了该设置的效果:
很麻烦,是不是?部分原因可能是因为使用了警告。每次当鼠标移动到图像之上时,都会弹出一个警告,而且必须要确认后这个警告窗才能消失。更不要提为了添加此处理程序而更改 HTML 中的全部 20 个 img 标记是多么繁琐了。
但更大的问题是可用性。最终,我们的 getImageDetails() 函数需要调用一个服务器端脚本 —— 而且针对每个鼠标移动事件都要这么做!这意味着即使是缩至第二列的第三个图像,只要是鼠标移至其上的每个图像,服务器都会相应动作。这太差劲了 — 太多不必要的处理负荷。
更好的做法是使用大 图像(带 “preview” 类)的 onClick 事件,这样,当单击图片库时,有关细节会被从服务器中拉出。但这意味着要从 20 个图像中删除鼠标移上处理程序并向另多出来的 20 个图像中添加一个新的处理程序;这太过混乱。让我们退一步以修复这个问题,请记住大多数 Ajax 应用程序都是极度动态的并且不依赖大量 HTML 更改。
如下所示的是一个简单的 JavaScript 函数(与所编写的其他的 JavaScript 代码一同放在 hoverbox-ajax.js),它遍历页面上的所有 img 标记,查找具有特定 CSS 类 “preview” 的所有图像。然后,每个图像的 onclick 处理程序再指向 getImageDetails 方法。太完美了!
function addImageHandlers() {
var imagesDiv = document.getElementById("images");
var imageElements = imagesDiv.getElementsByTagName("img");
for (var i=0; i |
现在就可以通过将其放入页面 body 元素的 onLoad 处理程序来运行它,如下所示:
这是一种极标准的 “Ajax 式” 的模式:以编程的方式添加事件处理程序,而不是在 HTML 中手动添加。这种模式最大的好处是可以很方便地进行更改,而不必触及代码中的全部 20(或 40!)个图像元素。
再次运行页面并注意,现在必须要单击每个图像以弹出警告框。更棒了!
接下来,我们需要利用我们的 JavaScript 函数找出所单击的图像的名称并确保该名称符合 PHP 服务器端脚本所要求的格式。最简单的部分是要将整个图像路径传递到此函数,原因是当调用某个 JavaScript 函数时,浏览器都会知道是页面上的哪个 元素调用的该函数。可以利用 this 关键词访问该元素。
要想看看其中的工作原理,可以按照如下所示更改 getImageDetails() 方法:
function getImageDetails() {
createRequest();
alert(this.src);
}
|
this 引用的是调用函数的那个图像元素,src 属性给出的则是图像的源属性。进行这些更改,重新加载页面后,结果应该如图 5 所示:
这是在正确方向上的一大进步!现在,我们的图片库可以调用一个 JavaScript 函数,获得图像的完整路径。
在调用服务器上的 PHP 脚本之前,只剩一个步骤要做。那就是将图像路径转变成只包含单一名称(即照片的名称),不包含任何路径信息或扩展名。所以对于 “file:///Users/bmclaugh/Documents/developerworks/ajax_2-backEnd/hoverbox/img/photo05.jpg”,希望发送给 PHP 脚本的只是 “photo05”。这实际上更像是一种 JavaScript 练习而非一个与 Ajax 相关的任务,代码如下所示:
function getImageName(imagePath) {
var splitPath = imagePath.split('/');
var fileName = splitPath[splitPath.length - 1];
return (fileName.split('.'))[0];
}
|
此函数接受图像路径并只获取名称部分。所以现在可以回过头来,稍许更新 getImageDetails() 函数:
function getImageDetails() {
createRequest();
alert(getImageName(this.src));
}
|
保存、重新加载,终于得到了我们所想要的:一个能获取图像名称并准备将其发给 PHP 服务器端脚本的处理程序。图 6 显示了此阶段的运行效果:
向 PHP 脚本发送图像名称非常容易。实现此目的所需要的只是一些 Ajax 101 之类的东西:
function getImageDetails() {
createRequest();
var url = "lookupImage.php?image=" + escape(getImageName(this.src));
request.open("GET", url, true);
request.onreadystatechange = showImageDetails;
request.send(null);
}
|
这与我们一直在使用的那个函数基本相同,不过作了几处更改而已。PHP 脚本的 URL 已经构建,图像名称也已添加完毕(进行了转义以确保传递过程中不会出现编程问题)。之后,请求也设置好了,回调函数也已分配,请求亦发送完毕。没有什么特别的,因为所有工作都已经由在上一节中所编写的 JavaScript 函数完成了。
|
本文假设您已经具备了一些基本的 Ajax 经验和熟练程度。如果不是这样,请参考 参考资料 部分。 掌握 Ajax 系列以简单易懂的方式给出了相关的基础知识,并会让您能够将本文所介绍的内容提升到应用程序级别。 | |
Ajax 应用程序最棘手的问题之一就是它们往往会出现在几个不同的地方:HTML 页面、处理样式及简单行为(如弹出图库中的较大图像)的 CSS、客户机上的 JavaScript、服务器上的 PHP 等等。要调试这些应用程序并不简单。
因此,一次只处理一件事情将会更好、更简单。例如,与其写一个用来获取服务器响应、解码并将其上传到页面的回调函数,不如简单地回应服务器响应并确保该响应正是所预期的。然后,可以添加客户端行为。基于此,我们先从回调函数开始:
function showImageDetails() {
if (request.readyState == 4) {
if (request.status == 200) {
var response = request.responseText;
alert(response);
}
}
}
|
记住,这个函数的名称必须与在 getImageDetails() 函数中指定给请求对象的名称相匹配。
尝试之后,应该可以得到如图 7 所示的响应(这里假您已将数据加载到 MySQL 数据库或已经更改了 PHP 脚本以返回虚拟数据。
看上去很不错!我们已经获取数据,现在只需将数据拆开并放入 Web 表单中相应的位置。
首先要做的就是用 JavaScript 的 split() 函数拆分来自服务器的数据:
function showImageDetails() {
if (request.readyState == 4) {
if (request.status == 200) {
var response = request.responseText;
var splitResponse = response.split('|');
var title = splitResponse[1];
var date = splitResponse[2];
var description = splitResponse[3];
}
}
}
|
split() 接受一个字符串并将其分成若干部分,再存入数组。因此,我们可以获取图像的标题、日期以及描述(注意 split() 返回一个基于零的数组。在本例中,splitResponse[0] 将返回图像的名称,而这正是我们首先发送给服务器的)。
利用 id 属性直接在 Web 页面上存取元素再简单不过了。将下面的 ID 添加到 Web 页面中图像明细部分:
id="info-title">Sunrise over the bay
id="info-date">27 October, 2006
id="info-text">Here is what I experienced when I took this photo.
|
获取这些元素 — 以更改其文本 — 十分简单。为每个元素使用 getElementById() 即可:
function showImageDetails() {
if (request.readyState == 4) {
if (request.status == 200) {
var response = request.responseText;
var splitResponse = response.split('|');
var title = splitResponse[1];
var date = splitResponse[2];
var description = splitResponse[3];
var titleElement = document.getElementById("info-title");
var dateElement = document.getElementById("info-date");
var descriptionElement = document.getElementById("info-text");
}
}
}
|
非常直观。现在剩下要做的就是用服务器的数据实际替代这些元素的文本。
改变 Web 页面上的某个元素中的文本看似 很简单,但实际上却需要费些心思。由于 Document Object Model (DOM) 将文本存为文本所在元素的子节点,事情因此变得复杂了。例如,在 p 中的文本实际上被存在了文本节点中,即表示 p 的元素节点的子节点。
因此,在结束 showImageDetails() 函数前,我们还需要一些有用的实用函数以便让处理 DOM 的工作能变得简单些。这些函数可用来清除一个节点内的文本,然后插入新文本。在 hoverbox-ajax.js 文件中添加这两个函数:
function replaceText(el, text) {
if (el != null) {
clearText(el);
var newNode = document.createTextNode(text);
el.appendChild(newNode);
}
}
function clearText(el) {
if (el != null) {
if (el.childNodes) {
for (var i=0; i |
妙处是可以将它们放入所有的 JavaScript 文件中(或将它们放进一个像 dom-utils.js 或 text-utils.js 这样的文件中)并且能在所有的应用程序中使用它们。
现在可以给 showImageDetails() 方法额外添加些简单内容以最终完成它:
function showImageDetails() {
if (request.readyState == 4) {
if (request.status == 200) {
var response = request.responseText;
var splitResponse = response.split('|');
var title = splitResponse[1];
var date = splitResponse[2];
var description = splitResponse[3];
var titleElement = document.getElementById("info-title");
var dateElement = document.getElementById("info-date");
var descriptionElement = document.getElementById("info-text");
replaceText(titleElement, title);
replaceText(dateElement, date);
replaceText(descriptionElement, description);
}
}
}
|
把所有的东西保存起来然后再重新加载,应该可以看到一个可喜的画面:一个工作中的、异步的图片库!图 8 显示的是我的结果画面:
您可以对它做更多的修改和进一步的调整。例如,右列的图像与文本有点重叠,就可以将图像与文本间的空间放宽。还可以针对具体的区域设置重新格式化日期,或更复杂些,基于浏览器所报告的来源地设置格式化日期。当然,本文只为 5 个图像提供了示例数据,因此,您可以为剩下的 15 个图像补充上数据。您可以自由发挥。
本文是一个小型系列文章中的第二篇,至此,我们已经得到了一个很棒的小 Ajax 应用程序。然而,本文更多关注的是 XmlHttpRequest 调用(如 send() 和 setRequestHeader() )以外的事情。实际上,这是大多数有经验的 Ajax 开发人员都知道的 “小秘密”:Ajax 只需要很简单的技术。使用一点 JavaScript — 甚至都不必使用像 Dojo 或 Prototype 这样的可以使事情变得更简单的现成库 — 您就差不多完成了 90% 的任务。若能再加入一些 DHTML 或是 DOM,您就是百分之百的专业人士了。当然我是指技术方面。
一个好的 Ajax 开发人员的衡量标准是他将如何处理应用程序的各个组成部分及其用户界面。应用程序的观感如何?(还记得我们在上一篇文章中曾讨论过为何可用性对 Ajax 应用程序如此关键么?)服务器端的组件只提供了最低数量的有效格式的数据?Ajax 代码由于必需要应付各种数据处理从而减缓了本应快捷的用户界面?再有,您很好地使用异步性了吗?在本文中,我们对应用程序进行了改动,让其只对应于单击事件来检索数据,因此,我们不会因鼠标在其上移动而为此图像拉出数据。这种可用性很好:在需要时,信息就会出现,但我们还不知道用户将会怎样使用这些信息。
|
developerWorks Ajax 资源中心 请访问 Ajax 资源中心,这里是免费工具、代码和有关开发 Ajax 应用程序的信息的一站式中心。这个 活跃的 Ajax 社区论坛 由 Ajax 专家 Jack Herrington 主持,通过这个论坛,您可以找到志同道合的人,并很可能从他那里获得您目前所存疑问的答案。 | |
总之,Ajax 更多地关乎设计和架构,而不是纯技术性的。通过 developerWorks 上其他的系列文章(比如我的 掌握 Ajax 系列,包括 12 个部分)可以了解更多具体细节,但需要强调的一点是正是 UI 设计、好的用户测验和清晰的思路才是获得成功 Ajax 应用程序的关键。举个例子:在很多实现中,Google Map 并没有真正的异步行为,并且处理界面中涉及了 “Ajax ” 的部分的代码也相对比较简单。但处理界面却要用去大量的工时,比如,东西要放在屏幕上的什么位置?有何选项可用?如何控制它们?这些与 Ajax 技术没有太大关系,但它们都会对应用程序产生影响。
在我们结束第 2 部分时,可以花一点时间调整一下此应用程序的用户界面。将文本信息移到左边;应用程序有什么变化么?如果想要单击图像后出现有关该图像的细节信息,又会怎样?整体印象有何不同?再试着处理服务器端组件;让返回的信息增加或减少。注意到有何变化么?
实际上若能在这个简单的示例上多花些时间,同样可以对什么能让应用程序 “可用” 以及什么可以导致应用程序 “沉闷” 有一个更好的理解。为了不冒犯他人,我就不罗列那些沉闷的在线站点了,但只要搜一搜,就能发现很多这样的站点。它们给人的感觉为何如此?可以从中学到什么教训?顺着这个思路走下去,您很快就能自己开发出很棒的 Ajax 应用程序了。
描述 |
名字 |
大小 |
下载方法 |
示例应用程序 |
wa-aj-backendsamples.zip |
280KB |
|
示例应用程序 |
wa-aj-backendhoverbox.zip |
282KB |
|
示例应用程序 |
w-aj-backendsampleData.sql.txt |
12KB |
|
|