清单 5. 将 XMLHttpRequest 创建代码移动到方法中
<script language="javascript" type="text/javascript">
var request;
function createRequest() {
}
</script>
如果按照这种方式编写代码,那么在处理 Ajax 之前需要调用该方法。因此还需要 清单 6 这样的代码。
清单 6. 使用 XMLHttpRequest 的创建方法
<script language="javascript" type="text/javascript">
var request;
function createRequest() {
}
function getCustomerInfo() {
}
</script>
此代码惟一的问题是推迟了错误通知,这也是多数 Ajax 程序员不采用这一方法的原因。假设一个复杂的表单有 10 或 15 个字段、选择框等,当用户在第 14 个字段(按照表单顺序从上到下)输入文本时要激活某些 Ajax 代码。这时候运行 getCustomerInfo() 尝试创建一个 XMLHttpRequest 对象,但(对于本例来说)失败了。然后向用户显示一条警告,明确地告诉他们不能使用该应用程序。但用户已经花费了很多时间在表单中输入数据!这是非常令人讨厌的,而讨厌显然不会吸引用户再次访问您的网站。
如果使用静态 JavaScript,用户在点击页面的时候很快就会看到错误信息。这样也很烦人,是不是?可能令用户错误地认为您的 Web 应用程序不能在他的浏览器上运行。不过,当然要比他们花费了 10 分钟输入信息之后再显示同样的错误要好。因此,我建议编写静态的代码,让用户尽可能早地发现问题。
用 XMLHttpRequest 发送请求
得到请求对象之后就可以进入请求/响应循环了。记住,XMLHttpRequest 惟一的目的是让您发送请求和接收响应。其他一切都是 JavaScript、CSS 或页面中其他代码的工作:改变用户界面、切换图像、解释服务器返回的数据。准备好 XMLHttpRequest 之后,就可以向服务器发送请求了。
欢迎使用沙箱
Ajax 采用一种沙箱安全模型。因此,Ajax 代码(具体来说就是 XMLHttpRequest 对象)只能对所在的同一个域发送请求。以后的文章中将进一步介绍安全和 Ajax,现在只要知道在本地机器上运行的代码只能对本地机器上的服务器端脚本发送请求。如果让 Ajax 代码在 www.breakneckpizza.com 上运行,则必须 www.breakneck.com 中运行的脚本发送请求。
设置服务器 URL
首先要确定连接的服务器的 URL。这并不是 Ajax 的特殊要求,但仍然是建立连接所必需的,显然现在您应该知道如何构造 URL 了。多数应用程序中都会结合一些静态数据和用户处理的表单中的数据来构造该 URL。比如,清单 7 中的 JavaScript 代码获取电话号码字段的值并用其构造 URL。
清单 7. 建立请求 URL
这里没有难懂的地方。首先,代码创建了一个新变量 phone,并把 ID 为 “phone” 的表单字段的值赋给它。清单 8 展示了这个表单的 XHTML,其中可以看到 phone 字段及其 id 属性。
清单 8. Break Neck Pizza 表单
还要注意,当用户输入电话号码或者改变电话号码时,将触发 清单 8 所示的 getCustomerInfo() 方法。该方法取得电话号码并构造存储在 url 变量中的 URL 字符串。记住,由于 Ajax 代码是沙箱型的,因而只能连接到同一个域,实际上 URL 中不需要域名。该例中的脚本名为 /cgi-local/lookupCustomer.php。最后,电话号码作为 GET 参数附加到该脚本中:"phone=" + escape(phone)。
如果以前没用见过 escape() 方法,它用于转义不能用明文正确发送的任何字符。比如,电话号码中的空格将被转换成字符 %20,从而能够在 URL 中传递这些字符。
可以根据需要添加任意多个参数。比如,如果需要增加另一个参数,只需要将其附加到 URL 中并用 “与”(&)字符分开 [第一个参数用问号(?)和脚本名分开]。
打开请求
有了要连接的 URL 后就可以配置请求了。可以用 XMLHttpRequest 对象的 open() 方法来完成。该方法有五个参数:
request-type:发送请求的类型。典型的值是 GET 或 POST,但也可以发送 HEAD 请求。
url:要连接的 URL。
asynch:如果希望使用异步连接则为 true,否则为 false。该参数是可选的,默认为 true。
username:如果需要身份验证,则可以在此指定用户名。该可选参数没有默认值。 password:如果需要身份验证,则可以在此指定口令。该可选参数没有默认值。
open() 是打开吗?
Internet 开发人员对 open() 方法到底做什么没有达成一致。但它实际上并不是打开一个请求。如果监控 XHTML/Ajax 页面及其连接脚本之间的网络和数据传递,当调用 open() 方法时将看不到任何通信。不清楚为何选用了这个名字,但显然不是一个好的选择。
通常使用其中的前三个参数。事实上,即使需要异步连接,也应该指定第三个参数为 “true”。这是默认值,但坚持明确指定请求是异步的还是同步的更容易理解。
将这些结合起来,通常会得到 清单 9 所示的一行代码。
清单 9. 打开请求
一旦设置好了 URL,其他就简单了。多数请求使用 GET 就够了(后面的文章中将看到需要使用 POST 的情况),再加上 URL,这就是使用 open() 方法需要的全部内容了。
挑战异步性
本系列的后面一篇文章中,我将用很多时间编写和使用异步代码,但是您应该明白为什么 open() 的最后一个参数这么重要。在一般的请求/响应模型中,比如 Web 1.0,客户机(浏览器或者本地机器上运行的代码)向服务器发出请求。该请求是同步的,换句话说,客户机等待服务器的响应。当客户机等待的时候,至少会用某种形式通知您在等待:
·沙漏(特别是 Windows 上)。
·旋转的皮球(通常在 Mac 机器上)。
·应用程序基本上冻结了,然后过一段时间光标变化了。
这正是 Web 应用程序让人感到笨拙或缓慢的原因 —— 缺乏真正的交互性。按下按钮时,应用程序实际上变得不能使用,直到刚刚触发的请求得到响应。如果请求需要大量服务器处理,那么等待的时间可能很长(至少在这个多处理器、DSL 没有等待的世界中是如此)。
而异步请求不等待服务器响应。发送请求后应用程序继续运行。用户仍然可以在 Web 表单中输入数据,甚至离开表单。没有旋转的皮球或者沙漏,应用程序也没有明显的冻结。服务器悄悄地响应请求,完成后告诉原来的请求者工作已经结束(具体的办法很快就会看到)。结果是,应用程序感觉不那么迟钝或者缓慢,而是响应迅速、交互性强,感觉快多了。这仅仅是 Web 2.0 的一部分,但它是很重要的一部分。所有老套的 GUI 组件和 Web 设计范型都不能克服缓慢、同步的请求/响应模型。
发送请求
一旦用 open() 配置好之后,就可以发送请求了。幸运的是,发送请求的方法的名称要比 open() 适当,它就是 send()。
send() 只有一个参数,就是要发送的内容。但是在考虑这个方法之前,回想一下前面已经通过 URL 本身发送过数据了:
var url = "/cgi-local/lookupCustomer.php?phone=" + escape(phone);
虽然可以使用 send() 发送数据,但也能通过 URL 本身发送数据。事实上,GET 请求(在典型的 Ajax 应用中大约占 80%)中,用 URL 发送数据要容易得多。如果需要发送安全信息或 XML,可能要考虑使用 send() 发送内容(本系列的后续文章中将讨论安全数据和 XML 消息)。如果不需要通过 send() 传递数据,则只要传递 null 作为该方法的参数即可。因此您会发现在本文中的例子中只需要这样发送请求(参见清单 10)。
清单 10. 发送请求
指定回调方法
现在我们所做的只有很少一点是新的、革命性的或异步的。必须承认,open() 方法中 “true” 这个小小的关键字建立了异步请求。但是除此之外,这些代码与用 Java servlet 及 JSP、PHP 或 Perl 编程没有什么两样。那么 Ajax 和 Web 2.0 最大的秘密是什么呢?秘密就在于 XMLHttpRequest 的一个简单属性 onreadystatechange。
首先一定要理解这些代码中的流程(如果需要请回顾 清单 10)。建立其请求然后发出请求。此外,因为是异步请求,所以 JavaScript 方法(例子中的 getCustomerInfo())不会等待服务器。因此代码将继续执行,就是说,将退出该方法而把控制返回给表单。用户可以继续输入信息,应用程序不会等待服务器。
这就提出了一个有趣的问题:服务器完成了请求之后会发生什么?答案是什么也不发生,至少对现在的代码而言如此!显然这样不行,因此服务器在完成通过 XMLHttpRequest 发送给它的请求处理之后需要某种指示说明怎么做。
在 JavaScript 中引用函数:
JavaScript 是一种弱类型的语言,可以用变量引用任何东西。因此如果声明了一个函数 updatePage(),JavaScript 也将该函数名看作是一个变量。换句话说,可用变量名 updatePage 在代码中引用函数。
清单 11. 设置回调方法
需要特别注意的是该属性在代码中设置的位置 —— 它是在调用 send() 之前设置的。发送请求之前必须设置该属性,这样服务器在回答完成请求之后才能查看该属性。现在剩下的就只有编写 updatePage() 方法了,这是本文最后一节要讨论的重点。
处理服务器响应
发送请求,用户高兴地使用 Web 表单(同时服务器在处理请求),而现在服务器完成了请求处理。服务器查看 onreadystatechange 属性确定要调用的方法。除此以外,可以将您的应用程序看作其他应用程序一样,无论是否异步。换句话说,不一定要采取特殊的动作编写响应服务器的方法,只需要改变表单,让用户访问另一个 URL 或者做响应服务器需要的任何事情。这一节我们重点讨论对服务器的响应和一种典型的动作 —— 即时改变用户看到的表单中的一部分。
回调和 Ajax
现在我们已经看到如何告诉服务器完成后应该做什么:将 XMLHttpRequest 对象的 onreadystatechange 属性设置为要运行的函数名。这样,当服务器处理完请求后就会自动调用该函数。也不需要担心该函数的任何参数。我们从一个简单的方法开始,如清单 12 所示。
清单 12. 回调方法的代码
它仅仅发出一些简单的警告,告诉您服务器什么时候完成了任务。在自己的网页中试验这些代码,然后在浏览器中打开(如果希望查看该例中的 XHTML,请参阅 清单 8)。输入电话号码然后离开该字段,将看到一个弹出的警告窗口(如 图 3 所示),但是点击 OK 又出现了……
图 3. 弹出警告的 Ajax 代码
根据浏览器的不同,在表单停止弹出警告之前会看到两次、三次甚至四次警告。这是怎么回事呢?原来我们还没有考虑 HTTP 就绪状态,这是请求/响应循环中的一个重要部分。
HTTP 就绪状态
前面提到,服务器在完成请求之后会在 XMLHttpRequest 的 onreadystatechange 属性中查找要调用的方法。这是真的,但还不完整。事实上,每当 HTTP 就绪状态改变时它都会调用该方法。这意味着什么呢?首先必须理解 HTTP 就绪状态。
HTTP 就绪状态表示请求的状态或情形。它用于确定该请求是否已经开始、是否得到了响应或者请求/响应模型是否已经完成。它还可以帮助确定读取服务器提供的响应文本或数据是否安全。在 Ajax 应用程序中需要了解五种就绪状态:
·0:请求没有发出(在调用 open() 之前)。
·1:请求已经建立但还没有发出(调用 send() 之前)。
·2:请求已经发出正在处理之中(这里通常可以从响应得到内容头部)。
·3:请求已经处理,响应中通常有部分数据可用,但是服务器还没有完成响应。
·4:响应已完成,可以访问服务器响应并使用它。
与大多数跨浏览器问题一样,这些就绪状态的使用也不尽一致。您也许期望任务就绪状态从 0 到 1、2、3 再到 4,但实际上很少是这种情况。一些浏览器从不报告 0 或 1 而直接从 2 开始,然后是 3 和 4。其他浏览器则报告所有的状态。还有一些则多次报告就绪状态 1。在上一节中看到,服务器多次调用 updatePage(),每次调用都会弹出警告框 —— 可能和预期的不同!
对于 Ajax 编程,需要直接处理的惟一状态就是就绪状态 4,它表示服务器响应已经完成,可以安全地使用响应数据了。基于此,回调方法中的第一行应该如清单 13 所示。
清单 13. 检查就绪状态
修改后就可以保证服务器的处理已经完成。尝试运行新版本的 Ajax 代码,现在就会看到与预期的一样,只显示一次警告信息了。
HTTP 状态码
虽然 清单 13 中的代码看起来似乎不错,但是还有一个问题 —— 如果服务器响应请求并完成了处理但是报告了一个错误怎么办?要知道,服务器端代码应该明白它是由 Ajax、JSP、普通 HTML 表单或其他类型的代码调用的,但只能使用传统的 Web 专用方法报告信息。而在 Web 世界中,HTTP 代码可以处理请求中可能发生的各种问题。
比方说,您肯定遇到过输入了错误的 URL 请求而得到 404 错误码的情形,它表示该页面不存在。这仅仅是 HTTP 请求能够收到的众多错误码中的一种(完整的状态码列表请参阅 参考资料中的链接)。表示所访问数据受到保护或者禁止访问的 403 和 401 也很常见。无论哪种情况,这些错误码都是从完成的响应得到的。换句话说,服务器履行了请求(即 HTTP 就绪状态是 4)但是没有返回客户机预期的数据。
因此除了就绪状态外,还需要检查 HTTP 状态。我们期望的状态码是 200,它表示一切顺利。如果就绪状态是 4 而且状态码是 200,就可以处理服务器的数据了,而且这些数据应该就是要求的数据(而不是错误或者其他有问题的信息)。因此还要在回调方法中增加状态检查,如清单 14 所示。
清单 14. 检查 HTTP 状态码
为了增加更健壮的错误处理并尽量避免过于复杂,可以增加一两个状态码检查,请看一看清单 15 中修改后的 updatePage() 版本。
联系客服