代码改变世界

AJAX 应用程序体系结构(第1部分)

2007-11-17 14:33  Valens  阅读(365)  评论(0编辑  收藏  举报
原文:http://msdn.microsoft.com/msdnmag/issues/07/09/CuttingEdge/Default.aspx?loc=zh

无论您是系统管理员、设计人员还是开发人员,您的工作都会受到 AJAX 的重大影响。管理员必须确保安全防护足够高,以便应对各种可能的新类型攻击。Intranet 管理员则必须保证任何浏览器上都没有禁用 JavaScript。Web 设计人员需要面对一些新的挑战,因为 AJAX 可实现的功能是以前无法实现或不切实际的。而开发人员则需要熟悉新的 API 和新的全面编程方法。尽管如此,AJAX 对架构师来说究竟有何意义?

 

AJAX 应用程序具有挑战性,因为它们引入了全新的概念和新的基础。AJAX 模式横跨客户端和服务器环境,因此架构师角色必不可少。对于确定客户端上发生的逻辑和处理与服务器上保留的内容,以及确定客户端和服务器上的哪些数据对象能够交换,清晰的体系结构设想显得举足轻重。


AJAX 派上用场

Web 最初用于共享信息的静态页面。短短几年之后,它发展成一种更加动态的媒介。如今,Web 由交互性极强的页面所组成;然而,它仍非常依赖于以整页转换为基础的模式。用户与页面进行的交互越多,必须创建及返回用户浏览器的页面就越频繁。以此方式使用交互式页面所引起的负面结果有很多,例如,减缓页面刷新和闪烁。这是一种不完善的解决方案,经常会导致糟糕的用户体验。AJAX 使得此模式中发生了非常令人渴望的变化,它允许更简洁的 Web 应用程序,并为生成新的应用程序奠定了基础。AJAX 以最终用户为核心。使用 AJAX 功能,Web 应用程序的交互性变得更强,响应更及时,更快速且更友好。交互和响应(操作缓慢时的反馈)都更妥善地得到了处理,便于用户对页面进行更多操作。总之,用户获得了更好的体验。

使用 AJAX,更多代码会在浏览器上执行,这要求将更多脚本指定给客户端页面。但是,这留下了一些大问题有待解决。何种脚本?谁将编写此脚本?此脚本遵循哪些体系结构原则和模式?

在本专栏中,我将从体系结构角度介绍 AJAX,帮助您、开发人员和架构师做出明智选择。同时,我将为设计人员和管理员提供足够的上下文信息,让他们的工作变得更加轻松。这是一个相当大的主题,因此我把它分为两部分来讲。请不要错过下个月的专栏,届时我将继续这一讨论。


AJAX 体系结构

如果您要考虑 AJAX,请看图 1,该图说明了这一步的体系结构含义。传统的 Web 应用程序需要在服务器上执行所有操作,应用程序只是偶尔会发出脚本代码,在客户端上运行任务,如处理数据验证。

图 1 传统 Web 模式与 AJAX 模式
图 1 传统 Web 模式与 AJAX 模式 (单击该图像获得较大视图)

AJAX 应用程序使用负责对 Web 服务器发出调用的客户端框架。AJAX 服务器端框架则负责请求数据馈送并将其返回客户端。这通常是 JavaScript Object Notation (JSON) 数据流,但也可使用其他格式,如 XML、RSS 和 CSV。

客户端接收数据馈送,并使用 JavaScript 更新 UI。该服务器通过返回以指定格式编码的原始数据,对请求做出响应。带宽消耗会降至最低,应用程序速度会提高(因为完成请求所用时间较少),而且 UI 更新也能在无可见回发的情况下生效。但是,在解决众多问题的同时,客户端上操作的增加也带来了新的问题,如新的编码实践、新的安全隐患、可访问性问题等。


什么是 AJAX 框架?

为了让 AJAX 在网页中有效,必须满足一些条件。首先,您的浏览器必须能够支持 JavaScript,此外还需要全时 Internet 连接,因为 AJAX 应用程序无法脱机工作。当所有请求都在页面级生成时,由于它们带有非 AJAX 应用程序,浏览器可通过这些页面的缓存集来提供导航,因此能够脱机进行。而在与使用 AJAX 应用程序的浏览器合作的基于脚本的框架中,同样的行为则必须进行显式编码。这一领域有很多新东西涌现,并且一些新的帮助工具正在开发中。让 AJAX 应用程序脱机工作,这对众多的软件供应商来说是一个挑战。

任何真正的 AJAX 应用程序都需要一个特定框架。通常,首选框架会在单独的客户端和服务器部分明确说明。

AJAX 页面中有两种主要类型的脚本代码:首选框架的系统级代码和实现页面预期行为的用户级代码。系统级代码提供用于向 Web 服务器发送异步请求并处理响应的引擎。用户级代码实质上使用文档对象模型 (DOM) 脚本更新页面的 UI。这里便遇到我先前提及的其中一个问题。由谁编写哪段?为讨论这点,我将重点介绍一种特别的 AJAX 框架 — ASP.NET AJAX Extensions。


ASP.NET AJAX Extensions

ASP.NET AJAX Extensions,即 ASP.NET 2.0 的一个扩展,为新网站和现有网站提供 AJAX 功能。ASP.NET AJAX 有两种编程模型:部分呈现和远程服务。它们应用程序中采用两个全然不同的体系结构,因而具有不同的优缺点。(您可能已经领会到,AJAX 中的多数功能都涉及权衡问题。)

简而言之,部分呈现允许您维护与传统 ASP.NET 2.0 应用程序类似的体系结构。它只提供了一套新的服务器端工具,您可以用其来实现无闪烁页面更新。

而远程服务则涉及由脚本相对较多的 AJAX 前端所调用的面向服务的后端。几乎所有的基本应用程序进程(包括身份验证、数据分页和分类)都必须重新设计。服务器端代码必须分解为特定于应用程序的服务,并且必须选择一种格式(如 JSON)用于数据交换。最后,必须安排一个前端,特别注意将您的编码限制为 UI 级任务,并保持多数业务逻辑避开客户端。

远程服务方法提供了更完整的 AJAX 体验,而部分呈现对现有应用程序中已有的功能进行渐进式增强,从而提供了更流畅的转换。

这两种方法在某种程度上可以结合使用。例如,您可以保留同一传统 Web 体系结构,在各处添加一些无闪烁更新,然后花时间以面向服务的方式重构某些关键功能。ASP.NET AJAX Extensions 提供了几个关键因素,但创建重要 AJAX 应用程序的主要工作还是取决于用户。图 2 显示了 ASP.NET AJAX Extensions 框架的客户端和服务器组件。


部分呈现一览

Jeff Prosise 在 2007 年 6 月的“超酷代码”专栏 (msdn.microsoft.com/msdnmag/issues/07/06/WickedCode) 提供了 ASP.NET AJAX 部分呈现的精彩讨论。从体系结构观点看,部分呈现不添加任何新内容。它只是使用某些 AJAX 功能来增强旧应用程序的一种绝佳方法 — 其中最重要的是无闪烁页面更新。

部分呈现不需要学习许多新技能,它对现有代码的影响也非常有限。对于可能会因采用 AJAX 而受影响的应用程序方面,例如安全性和可访问性,部分呈现还提供了简明的回调机制。一般来说,如果在升级过程中您无法将现有代码的某些部分转变成 AJAX,那么可以让这些代码块保持原样,然后继续。

部分呈现请求通常称为 AJAX 回发。该术语十分恰当,可准确表示实际发生的情况。AJAX 回发与传统 ASP.NET 回发相似,不同的是,前者由客户端 ASP.NET虽然表面上类似纯 AJAX 远程调用,但 AJAX 回发看上去好像是对 ASP.NET 运行时组件的普通回发请求 JAX 库中定义的一段脚本代码来执行。虽然表面上类似纯 AJAX 远程调用,但 AJAX 回发看上去好像是对 ASP.NET 运行时组件的普通回发请求。到达服务器之后,该请求便会经历典型回发请求的生命周期,并引发诸如 Init、Load 和 PreRender 等事件。在服务器上,AJAX 回发与传统 ASP.NET 回发的区别仅在于用来呈现最终标记的算法不同而已。不同的算法是提高性能和没有页面闪烁的关键。但是,该应用程序模型仍与 ASP.NET 中的相同。图 3 显示了 AJAX 回发请求的已修改的生命周期。

图 3 AJAX 回发的生命周期
图 3 AJAX 回发的生命周期

除了呈现阶段的实现之外,AJAX 回发和 ASP.NET 回发中的所有情况均相似。AJAX 回发仍会触发通知事件(如 Load 和 PreRender),并且它仍会处理视图状态信息,并引发状态更改和回发事件(如 TextChanged 和 Click)。

ASP.NET AJAX 页面必须包括 ScriptManager 控件的一个实例。此控件是 ASP.NET AJAX 页面真正的控制中心。它负责将页面链接到任何所需的框架脚本文件,并在检测到发生 AJAX 回发时协调部分呈现。ScriptManager 控件会检查请求中的 HTTP 头,以确定该请求是否为 AJAX 回发。以下是 MicrosoftAjaxWebForms.js 文件的摘录,该文件可在触发 AJAX 请求之前设置 HTTP 头:

   request.get_headers()['X-MicrosoftAjax'] =
'Delta=true';

 

要了解使得 AJAX 回发呈现阶段不同的因素,请仔细查看以下 System.Web.Extensions 程序集的摘录:

   protected override void OnPreRender(EventArgse){
base.OnPreRender(e);
if (IsInAsyncPostBack)
PageRequestManager.OnPreRender();
}

 

特别是,上述代码段均来自 ScriptManager 控件。如您所见,脚本管理器会注册一个预呈现事件处理程序。触发时,处理程序会检测它是否属于 AJAX 回发,如果是,则从 PageRequestManager 内部类调用一个方法。PageRequestManager 类的 OnPreRender 方法会执行以下操作:

   internal void OnPreRender()
{
_owner.SetRenderMethodDelegate(
newRenderMethod(this.RenderPageCallback));
}

 

该方法会设置特定的子例程来呈现当前请求的标记,非常简单。SetRenderMethodDelegate 是在 System.Web.UI.Control 类中定义的方法。此方法不作公用,它基本上分配一个事件处理程序,页面或控件使用该处理程序来呈现其内容。在先前的代码段中,该方法会在当前页面被调用,并以覆盖当前请求的整个呈现过程而结束。因而,负责 AJAX 回发标记的真正代码在 RenderPageCallback 方法(PageRequestManager 类中的内部方法)中定义。(有关 ScriptManager 控件的更多信息,请参阅本期《MSDN® 杂志》中 Ben Rush 的文章。)

基本上,AJAX 回发的已修改呈现过程会遍历页面上所有涉及回发并累积每一个标记的可更新面板。所有标记块会与隐藏字段和任何错误信息一起打包到响应流,并传递给客户端。现在让我们深入剖析示例页面上下文中的典型部分呈现调用。


剖析 AJAX 回发

若要使 ASP.NET 页面成为部分呈现的页面,首先必须向页面添加一个脚本管理器,然后通过使用 UpdatePanel 控件进行封装,从而定义可独立更新的区域。例如:

<asp:ScriptManager runat="server" />
<asp:UpdatePanel runat="server" ID="UpdatePanel1">
<ContentTemplate>
<%-- Markup of the region goes here --%>
</ContentTemplate>
</asp:UpdatePanel>

 

UpdatePanel 控件不会以任何方式改变为该区域生成的可见标记,而只是向原始标记添加一个外围 <div> 标记:

<div id="UpdatePanel1">
<%-- Markup of the region goes here --%>
</div>

 

什么会触发 AJAX 回发?如何进行管理?由谁管理?每当脚本管理器在页面上检测到一个或多个 UpdatePanel 控件时,它会发出如下的脚本程序块:

<script type="text/javascript">
Sys.WebForms.PageRequestManager._initialize('ScriptManager1',
document.getElementById('form1'));
Sys.WebForms.PageRequestManager.getInstance()._updateControls(
['tUpdatePanel1','tUpdatePanel2'], [], [], 90);
</script>

 

_initialize 方法是客户端 PageRequestManager 对象上的静态方法(参见 MicrosoftAjaxWebForms.js)。它会创建一个 PageRequestManager 类的全局实例,并将其初始化。该类充当一个单例,并且是后来可通过 getInstance 方法来检索的唯一可用实例。上述代码段的第二个语句向客户端框架注册了一组 UpdatePanel 控件。每个服务器端 UpdatePanel 控件都通过其 ID 进行引用。

此处发生的关键操作在 _initialize 方法内。如上所述,在创建该类的单个实例后,该代码会将其初始化。此时,还为 DOM 表单对象的提交事件注册了一个处理程序。这意味着,每当页面提交表单时,AJAX 脚本会介入并使用 XMLHttpRequest 发出请求,而不是让请求经历普通的浏览器回发。最初的表单字段集保持不变,并且为服务器端脚本管理器方便起见,会附加一些额外信息。因此,AJAX 回发上载的信息要比常规 ASP.NET 回发多一点。

视图状态以及任何其他隐藏字段随请求一起执行并上载到服务器。返回途中,会一同下载更新的视图状态与新的隐藏字段及可能更短的标记(如果有)。特别是,该响应只包括回发期间修改的可更新区域的标记。该列表包括触发回发的 UpdatePanel 控件(和所有嵌套面板)、页面中 UpdateMode 属性设置为 Always 的任何其他 UpdatePanel 控件,以及以编程方式刷新的所有 UpdatePanel 控件。以下代码是如何根据运行时情况以编程方式刷新面板的一个示例:

   UpdatePanel1.Update()

 

请考虑图 4 中所示的示例页面和返回的响应。您可以使用各种工具来监视入站和出站的 HTTP 数据包。此专栏中,我使用的是 Nikhil Kothari 的 Web Development Helper 工具,您可以从 projects.nikhilk.net/Projects/WebDevHelper.aspx 免费下载。

AJAX 回发的响应是一个文本流,可将其视为具有大小、类型、ID 和内容列的记录表格。每条记录都称为一个 delta 节点。图 5 显示了您获得的示例页面表格。

delta 节点标识了 AJAX 回发期间可能出现的更改。并非所有的响应都有相同的 delta 节点集,这取决于请求的服务器生命周期内发生的事件及原始标记的结构。

图 6 列出了目前支持的所有 delta 节点。负责处理 delta 表格的 JavaScript 代码位于 MicrosoftAjaxWebForms.js 文件中。


部分呈现的优化技术

查看图 6 之后,您会发现响应由两大块组成:标记和视图状态。事件验证数据(ASP.NET 2.0 的安全相关功能)、自定义隐藏字段、脚本和任何其他类型的节点通常总共只有几十个字节。视图状态的大小是 ASP.NET 页面的老问题了。遗憾的是,使用 ASP.NET AJAX 的部分呈现并未修正该问题。您仍需要精简您的视图状态,从而减少下载时间。那么如何减少页面的标记呢?实际上,您可以使用更小的面板来恢复特定点击所需的最少标记数量。

再次查看图 4 中的代码。页面中的逻辑相当简单,但一针见血。基本上,用户单击按钮时,TextBox 的所有内容都会用来更新标签。

页面只包含一个涵盖以下所有内容的 UpdatePanel 控件:文本框、按钮、分隔符和标签。逻辑上来说,这是可以接受的。如果您需要指定让页面中某个区域自行刷新,这是一个可供选择的方案。但是,在这种方式定义下,该区域包含了在回发中没有更新的重要部分代码。例如,TextBox 和 Button 控件不在此页面中更新,且 <hr> 分隔符为静态纯文本。

更好的方式是减少 UpdatePanel,以便包含唯一的 Label 控件 — 由按钮触发的任意回发中更新的唯一服务器控件。通常,您需要仔细确定控件之间的回发关系(基本上检查哪个更新哪个),并设计可更新面板,以包括用于指定用户操作的最少控件数。在图 4 所示的示例上下文中,有一个更佳方案:

<asp:TextBox runat="server" ID="TextBox1" />
<asp:Button runat="server" ID="Button1" Text="Update"
OnClick="Button1_Click" />
<hr />
<asp:UpdatePanel runat="server" ID="UpdatePanel1">
<ContentTemplate>
<asp:Label runat="server" ID="Label1" />
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="Button1" />
</Triggers>
</asp:UpdatePanel>

 

现在 UpdatePanel 控件只包含 Label 控件。这意味着文本框和按钮控件的(未更改)标记将不会返回。默认情况下,UpdatePanel 控件会在以下情况下刷新:它的任一个子控件引起回发或页面上另一 UpdatePanel 进行了刷新。 此行为可通过某些公共属性来更改,即 UpdateMode、ChildrenAsTriggers 和 Triggers。

特别是,UpdateMode 可决定 UpdatePanel 是设置为无条件刷新(默认情况)还是在特定条件下刷新。如果您希望有条件刷新(优化可更新面板的关键设置),请先将 UpdateMode 设置为 Conditional:

<asp:UpdatePanel runat="server" ID="UpdatePanel1"
UpdateMode="Conditional">

 

但是,请注意,有条件更新仅在页面上有多个面板时才适合。

某些情况下,您会发现很难将页面拆分为一组规范化的面板。在这些情况下,有一项技术可以帮助您防止子项触发回发。ChildrenAsTriggers 属性是一个布尔属性(默认值为 true),它决定面板的子控件是否充当 AJAX 回发事件的触发器。在以下代码段中,Button1 控件不会引起面板刷新:

<asp:UpdatePanel runat="server" ID="UpdatePanel1"
ChildrenAsTriggers="false">
<ContentTemplate>
<asp:Button runat="server" ID="Button1" Text="Update"
OnClick="Button1_Click" />
<asp:Label runat="server" ID="Label1" />
</ContentTemplate>
</asp:UpdatePanel>

 

在如此配置的面板中单击子按钮时,AJAX 回发仍然会执行与该按钮关联的回发代码,例如 Button1_Click 方法,但不会返回标记以刷新面板。

与有条件刷新相关的第三个属性是 Triggers。这是一个集合属性,列出了支配面板刷新的控件事件,如下所示:

<asp:UpdatePanel runat="server" ID="UpdatePanel1">
<ContentTemplate>
<asp:Label runat="server" ID="Label1" />
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="Button1"
EventName="Click" />
</Triggers>
</asp:UpdatePanel>

 

在本例中,Button1 的 Click 事件将刷新该面板。Button1 控件可以放在页面的任何位置。请注意,我谈论的是服务器事件。换言之,如果您要在用户更改下拉列表的选择时刷新面板,则必须先将 AutoPostBack 属性添加到该下拉列表。

AJAX 回发期间服务器上发生状态更改事件时,您仍可以配置一个面板进行刷新。请考虑以下代码:

<asp:TextBox runat="server" ID="TextBox1" />
<asp:UpdatePanel runat="server" ID="UpdatePanel1">
<ContentTemplate>
<asp:Label runat="server" ID="Label1" />
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger
ControlID="TextBox1"
EventName="TextChanged" />
</Triggers>
</asp:UpdatePanel>

 

每当 TextBox1 的内容更改时,标签都会被刷新:

Sub TextBox1_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles TextBox1.TextChanged
Label1.Text = TextBox1.Text
End Sub

 

TextChanged 事件以及其他状态更改事件,都是由回发触发的服务器端事件。该页面仍需要按钮或自动回发控件来触发回发,以便更新标签。

图 7 中的图表总结了将优化技术应用于可更新面板可能带来的好处。使用部分呈现,请求数据包的大小会稍微有所增加,但即使是优化最少的情况下,您得到的响应也比传统 ASP.NET 方案要小。而且毫无疑问,用户不会感觉到闪烁。

图 7 优化可更新面板的好处
图 7 优化可更新面板的好处 (单击该图像获得较大视图)

但是,需要提出警告的是,具有大量视图状态的页面不会从部分呈现中获益很多。您可以节省 5KB 的标记,但如果您需要 30KB 视图状态的情况,则无济于事。

但在某些情况下,视图状态仅仅是由其他原因引起的问题的无辜替罪羊。假设 ASP.NET 页面中有一个日历控件,您使用可更新面板使刷新保持流畅:

<asp:UpdatePanel runat="server" ID="UpdatePanel1">
<ContentTemplate>
<asp:Calendar runat="server" ID="Calendar1" />
</ContentTemplate>
</asp:UpdatePanel>

 

此片段的标记不超过 7KB,如果您添加一些样式,则可能增加到 10KB。现在,如果您禁用了该日历的视图状态,可节省大约 1KB 的数据,而这仅仅是总大小的 10%。这是因为,日历就标记而言是一个较大的控件。视图状态对它的影响微乎其微。ASP.NET AJAX 页面不应使用日历控件来实现日期选择功能。实际上,AJAX Control Toolkit 提供了一个扩展程序来代替,即 CalendarExtender 控件,当将其添加到普通文本框时,便可让您选择日期,有效地节省了 10KB 的标记。

每当文本框获得输入焦点时,此代码都会弹出一个日历:

<asp:textbox runat="server" ID="TextBox1" />
<act:CalendarExtender ID="CalendarExtender1" runat="server"
TargetControlID="TextBox1"
OnClientDateSelectionChanged="updateLabel"
Format="dd/MM/yyyy" />
<asp:UpdatePanel runat="server" ID="UpdatePanel1"
UpdateMode="Conditional">
<ContentTemplate>
<asp:label runat="server" ID="Label1" />
</ContentTemplate>
</asp:UpdatePanel>

 

该日历完全在客户端创建,只需下载几个小图标即可。该扩展程序会自动将选定日期写入对应的文本框。但是,您可以使用一些 JavaScript 代码来处理客户端日期选择事件和执行所需操作。


评价部分呈现

部分呈现是使用 AJAX 功能提升 ASP.NET 网站最快捷的方式。它既不需要新的技术,也不需要新的体系结构,相反,它适合逐步增强现有网站。但是与纯 AJAX 方式(对远程服务的直接客户端调用)相比,部分呈现的性能受到很大影响。

根据实际经验,我认为部分呈现是构建坚如磐石的网站首个 AJAX 版本的最佳方式。之后,您可以逐步从传统 ASP.NET 体系结构迁移,并开始构建后端服务和丰富的表示层。在“领先技术”安装的下一部分,我将深入探讨 AJAX 的远程服务方式。从体系结构上来说,这的确很棒,但它可能需要对应用程序进行全面的重新设计,并且会带来一些新的问题。

Dino Esposito是 Solid Quality Learning 的顾问,并且是《Programming Microsoft ASP.NET 2.0》(Microsoft ASP.NET 2.0 编程)(Microsoft Press,2005)一书的作者。Dino 定居于意大利,经常在世界各地的业内活动中发表演讲。若要与 Dino 联系,请发送电子邮件至 cutting@microsoft.com,或访问他在 weblogs.asp.net/despos 上开设的博客。

Subscribe  摘自 September 2007 期刊 MSDN Magazine.