与当今构建的绝大多数 Web 应用程序所采用的开发模式相比,AJAX 对 Web 解决方案架构师而言意味着一种模式转变。它立足于一些新的原则和规则来解释基于 Web 的系统的行为,并要求采用一些新的算法来实现它们。
AJAX 背后的主要原则是用户将纯数据发送到 Web 服务器,然后接收更多的纯数据。
AJAX 的第二个原则是用户自行协调操作,这将略过主机浏览器及其单页面请求/响应机制。
第三个 AJAX 原则是客户端代码利用从服务器接收到的纯数据来全面负责对用户界面的更新。
本专栏将为那些准备完全摆脱防御性 AJAX 实现(表现为部分呈现)的开发人员奠定基础。部分呈现是一种在处于 Web 窗体体系结构中时仍可实现某些 AJAX 功能的途径。AJAX 模式基于一种全新的原则,这种原则要求采用新的设计模式。
AJAX 模式的影响
ASP.NET 部分呈现是一种非常智能的添加内容,它属于传统的 Web 窗体回发模型。简而言之,使用部分呈现的页面其回发体系结构和页面生命周期与非 AJAX 页面中的完全相同(请参见图 1)。不同之处在于位于客户端的侦听器只阻止浏览器的默认操作(表单提交),并用 XMLHttpRequest 引导的 HTTP 请求来替代它(请参见图 2)。此方法不但可以节省用户的整页刷新时间,同时还可以节省开发人员在新体系结构和新模式培训方面所花的时间。
图 1 传统的整页回发操作 (单击该图像获得较大视图)
图 2 AJAX XMLHttpRequest 部分呈现 (单击该图像获得较大视图)
毋庸讳言,部分呈现在当前页面的上下文中提供了图形化优势。但是,如果您的应用程序是围绕一组独立的页面设计的,那么从一个页面到下一个页面的过渡仍需要整页加载。部分呈现非常适合于替换内部页面回发,例如,对网格进行分页、变更选择后调整用户界面或就地编辑记录。
由于部分呈现只是一种更智能形式的页面回发,因此它具有与 Web 窗体模型相同的体系结构限制。例如,每个浏览器窗口只能有一个待决请求。对于当前的 Web 应用程序(提交表单、获取新页面)模型而言,此限制可能不会产生太大问题。
但是,随着您对 AJAX 认识的拓展,自然而言就会期望用户可以同时执行多个活动。仅依靠部分呈现,用户是无法同时执行两个异步操作并将其完成的。
部 分呈现只能每次执行一个请求操作以保持视图状态的一致性(一致性仍是模型不可或缺的组成部分)。这严重违背了 AJAX 中的第一个要求:异步。使用部分呈现(带外执行),可以实现 JavaScript 驱动的回发,但每次只能有一个。如果在前一个操作完成之前触发了第二个操作,则待处理操作将被中止,以便新操作可以继续执行。可通过编程方式将这一后进得 胜策略改为先进得胜模型,从而使当前操作仍保持活动状态,而新操作将被系统取消 — 实质上这仍然是每次只能执行一个操作。
单页界面模型
为 了充分利用 AJAX,必须在单一页面中包含其中的全部功能,或者是至少包含大部分功能。这称作单页界面 (SPI) 模型。在 SPI 模型中,浏览器与 Web 应用程序的所有交互只能在一个页面的范围内进行。此方法对 Web 来说是一种创新,但对于 Windows® 和桌面开发却并不陌生。SPI 其实模型就像是具有主(并且唯一)窗口的 Windows 应用程序一样。
在 SPI 模型中,主页面是可以独立加载、更新和替换的一些可视元素的组合(请参见图 3)。通过这种方式,可以不必在每次用户操作后重新加载整个页面。在任何时候,都只显示与应用程序当前阶段相关的可视元素和内容。其他所有内容均被隐藏;但只要应用程序流程中需要用到它,它就会显示出来。
图 3 页面中的单页界面元素 (单击该图像获得较大视图)
SPI 模型自然会启用许多交互性很强的功能,其中包括就地编辑、上下文相关的用户界面、即时用户反馈提示以及异步操作等。但 SPI 模型并不仅仅具有出色的性能或响应能力,它的主要优势在于针对用户体验方面做的重大改进。
不过,即使它拥有如此多的优点,您还是应该清楚,使用 SPI 模型设计新应用程序是一项具有挑战性的工作,因为没有任何现成的模式和最佳实践。目前的结论是有一种使用 AJAX 的简单方法,它适合于众多公司和多种情况。而当前主流的 AJAX 使用方法不太便利。
为 了构建纯 AJAX 应用程序,您需要一个良好的用户界面小组件库来提供各种效果和特殊行为。您需要一个内容丰富而又可以自定义的文档对象模型 (DOM),此模型使用标准的万维网联盟 (W3C) DOM 作为其基础引擎,但它允许您定义自己的特定应用程序的模型。最后,您还需要一个服务器和客户端框架,以便轻松有效地开发用户界面和源代码脚本。最好还准备 一些用于调试和测试所有这一切的工具。
Microsoft 和其他一些供应商提供了部分这样的工具。有许多 UI 小组件库可供您选择,此外还有一些供应商,它们生产各种用来定义其各自客户端对象模型的控件。而真正有帮助的则是改进后的专门设计用作 Web 窗体备选模型的 AJAX 框架,它的灵感源自 SPI 模型。
其 中一个此类替代编程模型是模型视图控制器 (MVC) 框架,在 ASP.NET 3.5 扩展库中可以找到它。但就目前来看,它并没有太多值得 AJAX 借鉴的内容,虽然它不会阻止任何 AJAX 功能的实现,但它也似乎没有发展成为 SPI 模型的打算。不过时间会证明一切的。
在 SPI 模型中,主页面指向同一应用程序中的 HTTP 端点。它执行远程代码,但不会重新加载整个页面。而且,它还会使用生成 HTML 和脚本的控件来更新用户界面。这些控件非常智能,可以生成它们需要的许多 JavaScript 代码。例如,假定有一个用来预订航班的表单,其中可能有两个参数需要搜索:时间或成本。如果用户对便宜的航班更感兴趣,那么您就不必显示时间下拉列表。如 果用户需要在准确的时间起飞,那么您就必须调出显示时间的 HTML。
很明显,只需要一些 JavaScript 就可以轻松实现这种显示/隐藏技巧。但是现在,这些代码需要由页面开发人员来负责编写。ASP.NET AJAX 控件工具包 (
asp.net/AJAX/AjaxControlToolkit/Samples/CollapsiblePanel/CollapsiblePanel.aspx) 中提供的控件(如 CollapsiblePanel 控件)也可以帮助实现此操作。
单页界面模型的缺点
虽 然 SPI 模型(及其包含的整个 AJAX 模式)带来了交互性更强的用户体验,但同时也引发了诸多问题,例如可搜索性、历史管理、可访问性和脱机支持等。多年以来,一直使用永久链接来跟踪 Web 页面。搜索引擎只需将一组关键字映射到一个或多个 URL 即可构建其业务。此模型的工作前提是假设 Web 应用程序中每个状态都对应一个页面和一个不同的 URL。
如 果 SPI 模型处在 AJAX 中,则此假设不再有效。如果所有一切(或大部分)都发生在同一页面中,则没有 URL 转换来标记不同的状态和不同的站点内容。因此,没有将内容(和关键字)与唯一 URL 相关联的简单方法。SPI 模型的这一特性会影响可搜索性以及历史管理(例如,使用“后退”和“前进”按钮的能力)。
浏览器历史记录最终可能会变为过时的概念,它们是传统静态 Web 模型的搭档。但是您必须要知道,用户对这些按钮实在是太熟悉了,简单地禁用它们并不是一个可行的办法。
可访问性是 AJAX 应用程序的另一个大问题。大多数流行的屏幕读取器在处理通过 DOM 脚本生成的任何内容时都面临很大的困难。按照设计,所有形式的 AJAX(包括部分呈现)都严重依赖于 DOM 脚本。因此,您需要通过其他途径来解决可访问性问题。
Web Content Accessibility Guidelines (WCAG) 中的第 508 节建议,只要页面利用脚本语言来显示内容或创建可视元素,它所提供的替代功能文本就应该可以通过辅助技术加以访问。为符合此建议,需要使用 <noscript> 标记。当前,大多数 AJAX 框架都在页面内部进行动态更新,而不更新 <noscript> 标记中包含的静态信息。
可访问的富 Internet 应用程序
此 问题看起来好像是双重的,既涉及 AJAX 应用程序,又涉及屏幕读取器背后的技术。一方面,屏幕读取器要了解一些客户端事件,如 onclick、keypress 和 readystatechange 等。在屏幕读取器中弄清了这些事件后,便可以至少将第一个可访问层添加到大量基于 AJAX 的应用程序中。
另 一方面,如果屏幕读取器超出了 DOM 中的初始页面加载事件范围,它们通常不会发出任何新信息。但您可以使用一些技巧让屏幕读取器知道有些内容已经发生了变化。最有效的一招就是在更新后的 DOM 树的根目录将 tabindex 设置为 -1。但这只是一个小技巧,可访问性的解决需要更为通用的 AJAX 解决方案。
其 中的一种可行解决方案是 W3C 正在制定的被称为“可访问富 Internet 应用程序”(ARIA) 的新标准,它主要由 HTML 标记的特定读取器扩展组成。一些比较流行的客户端 AJAX 库已经开始支持某些 ARIA 功能了。但一般来说,这并不仅仅是有关 AJAX 可访问性兼容标准的可用性问题。它还常常涉及服务器控件实际生成的内容(标记和脚本)。
对 于脱机应用程序是怎样的情况?许多开发人员认为脱机 AJAX 应用程序不可能实现,因为 AJAX 应用程序仍然完全基于 Internet。在我看来,这种说法虽然并非完全错误,但也有点过于简单化了。现在,几乎所有 Web 应用程序(AJAX 和非 AJAX)都基于 Internet 或 intranet。
但 是,非 AJAX 应用程序的确可以脱机工作。例如,对于历史记录管理,启用脱机导航的魔力完全在于浏览器。在传统的 Web 应用程序中,每个 HTTP 请求都由浏览器来管理。如果没有可用连接(在引发 HTTP 404 错误之前),浏览器会在其本地页面缓存中进行查找。
AJAX 应用程序不同于传统的 Web 应用程序,它使用 XMLHttpRequest 对象而非浏览器引擎来发送 HTTP 请求。对于支持脱机情况的 AJAX 应用程序,您只需为 XMLHttpRequest 对象赋予访问浏览器缓存的权限或赋予创建并管理其自身已访问页面的缓存的能力即可。某些 AJAX 框架已开始引入此功能。但这并非是一件容易的事,因为它涉及从 JavaScript 代码访问磁盘。
AJAX 模式概述
下 一代基于 Web 的应用程序将直接面向 AJAX,而且会更多直接面向富 Internet 应用程序 (RIA)。起初,引领这一革新版本的是以下三大类应用程序:基于 HTML 的传统站点、为了将多个系统集成到一个基于 Web 的前端而构建的混合应用程序、胖客户端。
对于第一个类别,部分用户界面加载(部分呈现)是实现 AJAX 的一种简便方法,它对现有代码和技能的影响非常小。
而 混合程序的概念本身未必适合于 AJAX 和改进的用户交互。它只是一种从各种来源收集数据并将其一并放入一个统一而又一致的用户界面中的方法。此操作也可以在传统的服务器对服务器的情况下执行。 但是坦率地说,AJAX 可以使其更加简单有效。从 AJAX 角度看,混合应用程序需要标准的数据序列化格式(整合)、用于通过脚本调用远程服务的轻量级框架、可更新的 DOM,可能还有一些具有方便的编程模型的富可视控件。
使 用 AJAX 构建胖客户端是一个最严峻的挑战。胖客户端可以是分布式企业系统的前端,也可以是业务线应用程序的表现逻辑层。它还可以是 IT 部门决定作为 Web 应用程序而公开的独立应用程序。这些应用程序无论是发布到 Internet 上还是限制在 intranet 内,都需要常规桌面 UI 所具有的丰富性和速度。
与 Windows 开发相比,在交互性和响应性方面 Web 开发都是一种倒退。利用 AJAX,您最终可以使用其中的工具(和环境条件)向本质上不同的模型演化。但其中存在着不容忽视的折衷。一方面,有数百万的用户和开发人员已习惯了使用 旧的 Web 及其历史模式、脱机导航、收藏夹、单一操作、页面转换和永久链接等。另一方面,您采用的却是 AJAX 及其并行操作模式和单一自动更新用户界面功能。在这种情况下,对于用户任务,AJAX 模型需要真正的恢复模型,而不仅仅是使用浏览器的历史记录和页面导航功能。
要对 SPI 模型编写代码,需要使用一组新的设计模式。图 4 列出了一些最流行的 AJAX 模式。正如您所看到的,其中大部分都侧重于用户界面技术和排列,但您也应该注意到,此列表中并不包括一些已在 Microsoft® AJAX 客户端库中实现的 AJAX 流行模式和实践。例如,如果您使用 ASP.NET AJAX,则不需要 AJAX 存根、跨浏览器 JavaScript 模型或调用跟踪模型,因为所有这些功能都是预置的。

Figure 4 一些 AJAX 模式
| 模式 |
目标 |
| 浏览器端模板化 |
此模式建议使用 HTML 模板,它们将使用从远程 HTTP 端点检索的数据动态填充。此模式建议您设置自己的模板层,而不要基于每个请求动态为数据重新生成 HTML 布局。此模式是 HTML Message 模式的替代方案。 |
| 跨域代理 |
此模式对可访问的、公开提供的服务运行服务器到服务器连接,并将数据传回客户端。在客户端浏览器中,不允许 AJAX 应用程序连接到页面域外部的任何 URL。但是,同一域内的本地代理可以轻松地从任何位置获取数据并将其返给调用方。 |
| 检测信号 |
由于许多 AJAX 应用程序可能会在客户端执行大量操作而不传回信息,因此可能需要通知服务器指定客户端仍处于活动状态。此模式建议客户端应用程序要定期上载“检测信号”消息,以指示应用程序仍在载入状态并在浏览器中正常运行。 |
| HTML 消息 |
远 程 HTTP 端点通常会返回要集成到现有 DOM 中的客户端 JavaScript Object Notation (JSON) 数据。此任务只能通过 JavaScript 来完成。但是,如果客户端代码特别复杂或考虑到性能问题,您可能希望从服务器返回 HTML(数据和布局)而不是纯数据。 |
| 微链接 |
AJAX 主要用于在同一页面内执行大量活动。那么如何引用外部内容(即在传统 Web 应用程序中会转到不同页面的内容)?您需要一种页内超链接或称为微链接。微链接是对标记块的引用,它通过服务器调用进行检索然后再插入到页面中。微链接可 以是 HTTP 端点,也可以是 JavaScript 命令对象的方法。 |
| 点播 JavaScript |
这 是流行的延迟加载模式的 JavaScript 版本,通常用于数据访问层。在页面初始化过程中下载所需的全部 JavaScript 可能会对性能产生影响,从而拖慢整个进程。通过采用按请求来加载 JavaScript 文件的方法,可以使页面获得更快的加载速度,同时还不会对功能产生影响。 |
| 页面安排 |
由于大部分应用程序的活动都发生在同一个页面中,因此您需要更新页面的内容并随着上下文内容的变化来显示最新信息。此模式只建议您使用 DOM 来添加/删除或显示/隐藏元素以反映其状态的转换。 |
| 定期刷新 |
浏览器会定期安排请求以获得最新信息,并用它来刷新用户界面。 |
| 弹出框 |
此模式代表 Web 版本的模式/无模式 Windows 对话框。弹出框由一些 HTML 内容组成,它们显示在现有内容的前面,显示时间可以很短,也可以一直显示直到用户将其取消。 |
| 预取模式 |
此模式建议预测最可能的用户操作并预先获取所需的数据。实现此模式需要付出一定的代价:毕竟它只是猜测,有时可能会预测错误。尽管可以有效地提高感知性能,但如果实现效果不佳或者由于严重的服务器带宽损耗致使达不到理想情况,则此模式也可能会带来性能损失。 |
| 进度指示器 |
此模式用来监视服务器操作的进度。其思路是,服务器操作将其自身的进度写入一个共享位置,而客户端监控服务则可在进度刷新时进行读取。 |
| 提交限制 |
AJAX 的其中一个潜在缺陷是在单位时间内可能会生成过多的服务器请求。如果出现这种情况,则说明存在着明显的可伸缩性问题。此模式建议您使用定时器将数据定期上载到服务器和本地缓存或队列中,以将请求累积起来。 |
| 超时 |
如果从客户端执行一些重量级操作(如流式或定期刷新),则要想保证每个连接的客户端都真正使用此应用程序会有一些困难。此模式建议您使繁重操作超时,在用户提出明确请求时再恢复。 |
| 唯一 URL |
对于反映不同状态的应用程序的不同部分,此模式允许您为其分配不同的 URL。此模式通常用于支持 AJAX 应用程序中的历史记录。 |
| 虚拟工作区 |
服务器需要尽快响应请求,但由于带宽原因,它可能不必返回所有可用的数据。此模式建议构建一个虚拟用户界面,即使数据在客户端只存在一小部分,也可以实现使所有数据都可用的目的。应用程序会负责根据需要下载数据并将其缓存到本地。 |
您还应注意,在图 4 列出的模式中(一般为 AJAX 模式),参考模式要多于设计模式。它们指出了常见的操作执行方法,但并非它们都是设计问题。
在
图 4 中,我简要概述了每个模式的要点。在本专栏的剩余部分,我要对其中的部分模式进行深入的分析。在以后的专栏中,我将回到其他一些重要模式,讨论其驱动力并演示一些执行方法。(要了解有关 AJAX 模式的详细信息,可以访问一个非常出色的网站
ajaxpatterns.org。)
唯一 URL 模式
URL 是 Web 的基础。用户可以将中意的 URL 保存下来以供将来参考、可以按照 URL 所指开始新的内容体验,此外还可以使用 URL 回到先前的状态。在 AJAX 和 SPI 模型中,应用程序可以在单个 URL 中完成许多任务。这将使 Web 体验的核心支柱面临彻底的改变,即:应用程序的离散状态是由不同的 URL 来标识的。
浏 览器会在用户浏览时构建其各自的 URL 缓存。但如果使用 AJAX,许多操作都不通过浏览器,因此不会被缓存到访问过的 URL 列表中(在此列表中可驱动“后退”和“前进”菜单)。另一方面,客户端浏览器不会提供将 URL 添加到列表中的 JavaScript 代码编程模型。在现有列表中,浏览器对象模型只会提供向前和向后的导航方法。
“唯一 URL”模式为每个重要的应用程序状态都分配一个唯一的、含义鲜明的 URL。例如,如果用户在 AJAX 页面中通过单击来编辑某个值,则新 URL 会被添加到浏览器缓存中,即使此操作是通过 XMLHttpRequest 在同一页面中执行的。
可使用以下 JavaScript 来更改 URL 而无需重新加载页面:
window.location.hash = stateInfo;
此代码的作用是将以 # 作为前缀的片段添加到 URL 中,如下所示:
http://www.contoso.com/shopping.aspx#edit-1234
使用此模式时,URL 实际上是在您启动任何给定的 AJAX 操作时发生改变的,因此可以通过浏览器来跟踪应用程序状态的变化。
但 是,要执行的操作远不止捕获 URL 这样简单。如果浏览器被定向到基于哈希值的 URL,它首先会加载主 URL,然后再查找具有此哈希名称的页面段。在 AJAX 上下文中,哈希名称并不指向实际的页面段,而是指向代表当前状态的特定于应用程序的信息。例如,edit-1234 可能表示您正在编辑的项目其 ID 是 1234。实际格式完全取决于您。
如 果浏览器找不到适当的段,则它将忽略 URL 哈希值。这样,用户会加载该页面,但可能不是以预期的应用程序状态。此外还需要另一个技巧。您应截取页面的 onload 事件、分析 URL、提取哈希值并运行将页面置于期望状态所需的 JavaScript 代码,如下所示:
window.onload = function() {
checkAndParseURL();
}
checkAndParseURL() {
var state = window.location.hash;
restorePage(state);
}
在 ASP.NET 3.5 扩展所提供的历史记录支持中也采用了类似的方法。您可以访问 quickstarts.asp.net/3-5-extensions/ajax 了解更多相关信息。ASP.NET 3.5 扩展中的解决方案已完全集成到此框架中。它将以添加到 ScriptManager 控件的新属性和事件的形式显示出来。但最终,它将是“唯一 URL”模式的具体实现。
另外还应注意,基于 URL 哈希值的技巧对 Internet Explorer
® 无效,因为 Internet Explorer 无法识别出 URL 哈希值的变化,除非是内嵌帧。实际上,所有浏览器在处理段导航方面所表现出的行为特点都是互不相同的(有关本主题的详细信息,请参阅
weblogs.asp.net/bleroy/archive/2007/09/07/how-to-build-a-cross-browser-history-management-system.aspx)。ASP.NET 3.5 扩展中的解决方案考虑到了此差别,这使得它成为真正的跨浏览器技巧。
超时模式
AJAX 的其中一个最大优点是可以实现实时页面更新。但是,实时更新如果被误用,可能会成为应用程序的严重威胁。假设某个用户显示了一个活动页面,此页面每隔几秒 钟轮询一次服务器以更新一些内容。如果该用户离开几个小时,但没有关闭浏览器。这样做产生的结果是,页面不断发送请求,给服务器带来大量(而且无用)的工 作负荷。
如 何能够确定客户端会话是否超时?在服务器上存在着会话超时,而在 AJAX 中也存在着不容忽视的客户端会话。要检测客户端会话的结束,您需要检查在给定时间段内是否有用户活动(例如单击和敲击)。监视键盘和鼠标活动的任务可能会 非常繁重;我们通常都采取一种基于定时器的方法,这种方法不但简单而且很有效。
要根据定时器检测会话的结束,应将客户端定时器设置为经过指定秒数(实际当中更有可能是分钟数)之后过期、停止正在执行的任务并弹出一个警告框。如果用户对提示做出响应,则会照例重新开始处理。
图 5 显示了部分 JavaScript,它描述了超时模式的本质。在示例页中嵌入了一个时钟。此时钟可使用 UpdatePanel 中的 Label 控件来获取,并由 Timer 控件定期更新,如下所示:

Figure 5 实现客户端会话超时
<script type="text/javascript">
var timer = null;
function pageLoad()
{
if (timer === null)
{
timer = new Samples.TaskTimer(5000, stopTask);
timer.start();
}
}
function pageUnload()
{
if (timer != null)
timer.stop();
}
function stopTask()
{
// Stop the clock
var clock = $find("<%= Timer1.ClientID%>");
clock._stopTimer();
AskIfTheUserWantsToContinue();
}
function AskIfTheUserWantsToContinue()
{
// Ask if the user wants to continue
var answer = window.confirm(
"Is it OK to continue with the clock?");
if (answer)
{
// Restart the task
var clock = $find("<%= Timer1.ClientID%>");
clock._startTimer();
// Restart our own timeout engine
if (timer !== null)
timer.start();
return;
}
}
</script>
protected void Timer1_Tick(object sender, EventArgs e)
{
Label1.Text = DateTime.Now.ToLongTimeString();
}
此时钟代表一项繁重的任务,可能会造成服务器请求溢出。其思路是设置一个定时器,定期询问用户是否确实想要继续运行时钟。超时代码首先会停止时钟,然后显示消息框。得到用户响应后,时钟重新启动。
此代码利用 $find 函数来定位 ASP.NET AJAX 组件,在本例中,它还是 ASP.NET 定时器服务器控件的客户端对象模型。图 6 显示的是正在运行的页面,它会弹出一个框,询问用户是否确定继续。
图 6
询问用户是否继续 (单击该图像获得较大视图)
代码下载 (203 KB)