使用 ASP.NET AJAX Extensions 将您的站点过渡到 AJAX 体验时,有两个主要的编程模型可供选择:部分呈现和脚本服务。在上个月的专栏中,我主要从体系结构的角度论述了部分呈现。简而言之,使用部分呈现,您无需更改 ASP.NET 应用程序的底层体系结构——它是实现 AJAX 某些最佳元素(如站点页面的无闪烁更新)的便捷途径。实现此类改进行为只需添加一些新的服务器控件(特别是 ScriptManager 和 UpdatePanel),并让它们悄悄地施展一些技巧,通过 XMLHttpRequest 对象运行的异步请求来转换传统的回在发。此方法很容易实现,因为它只是将 AJAX 功能应用于现有的 Web 开发模型。
如果您准备对构建 AJAX 应用程序实行全面的模式转换,那么就应该了解一下脚本服务的方法。总的来说,典型的 AJAX 体系结构相当容易理解。图 1 展示了其工作原理的高层视图。其中有一个由应用程序特定服务组成的后端,通常只是可调用 AJAX 脚本的外层,其下方是业务逻辑所在和发挥作用的系统中间层。服务与前端通过 HTTP 交换数据,使用多种格式传递参数和返回值。前端由运行于客户端上的 JavaScript 代码组成,在接收和处理完数据后,它面临着使用 HTML 和 JavaScript 构建图形用户界面的重大任务。对 JavaScript 的依赖是由于受浏览器结构的限制,只有当浏览器可以支持功能更加强大的编程功能时,这种情况才会改变。
通过 AJAX 登录
抛弃传统的 ASP.NET 模型会引起许多实际反响。考虑一下登录过程,看看在纯 AJAX 解决方案中会发生怎样的变化。
当前对 ASP.NET 来说,启用登录过程包括:用 Login 控件配置登录页面,用 LoginView 控件配置受保护的页面,设置 ASP.NET 成员身份提供程序。受保护的页面将使用登录模板或匿名模板以图形方式反映身份验证过程的结果。在 ASP.NET 2.0 中,您无需编写任何代码即可完成大部分此类工作。要成功地进行登录,需要有用户到达受保护页面时发生的重定向 (HTTP 302)、从登录页面到身份验证凭据的回发,然后是回到原先请求页面的另一次重定向。
对于 AJAX 页面来说则并不一定如此。如果用户从地址栏请求一个受保护的页面,除了重复 ASP.NET 的历程之外,您并不能做什么。不过,如果您在页面中有指向受保护页面的链接,则可以将某些脚本添加到该链接的 onclick 事件上,以检查用户是否已通过身份验证。如果没有,则可以弹出一个警告框以提醒用户,如下所示:
function checkFirst()
{
var loggedIn = Sys.Services.AuthenticationService.get_isLoggedIn();
if (!loggedIn)
{
alert("You must be logged in to follow this link");
return false;
}
return true;
}
如您所见,除了其他方面之外,此方法使用一个客户端框架来帮助您检查当前用户是否已登录。凭借 ASP.NET AJAX Extensions 即可通过 Sys.Services.AuthenticationService 类实现该功能。
同样的服务可用于在用户指定有效凭据后对其进行身份验证。在这种情况下,不发生任何重定向和回发。一切都发生在同一页面的环境中,如下面的代码段所示:
function OnLogin()
{
Sys.Services.AuthenticationService.login(
$get("UserName").value,
$get("Password").value,
false,
null,
null,
OnLoginCompleted,
OnLoginFailed);
}
检查凭据成为使用脚本服务进行身份验证的标准脚本操作。这完全要靠开发人员来更新客户端页面的 UI,以反映身份验证成功后用户已处于登录状态。除非您使用的框架提供了一个将 HTML 包装在控件中的对象模型,否则,编写文档对象模型 (DOM) 脚本的过程要长得多。注意,ASP.NET AJAX Extensions 目前尚不提供用控件代表标记块的对象模型。
服务的风格
在 AJAX 中,服务表示驻留在应用程序域并向客户端脚本代码公开功能的一段代码。AJAX 中使用的服务需要进行某种设计,以便实现对实际应用程序后端和中间层的保护,避免其与最终用户直接交互。此类服务是 WS-* Web 服务吗?它可以是面向服务的体系结构 (SOA) 服务吗?
最适合 AJAX 应用程序的服务主要涉及向 Web 客户端公开数据和资源。它可以通过 HTTP 获得,并要求客户端使用 URL(也可以是 HTTP 头)访问数据和命令操作。客户端与服务进行交互使用的是 HTTP 动词,如 GET、POST、PUT 和 DELETE。换句话说,URL 代表一个资源,而 HTTP 动词描述了您想对资源采取的操作。在这些交互中交换的数据以简单格式表示,如 JSON 和纯 XML,也可以整合格式表示,如 RSS 和 ATOM。
具有这些特征的服务是 Representational State Transfer (REST) 服务。有关 REST 定义的详细信息,请阅读描述 REST 前景的原作(在 ics.uci.edu/~fielding/pubs/dissertation/top.htm 中提供)。
最后,AJAX 应用程序所使用的服务并不倾向于使用 SOAP 进行通信,而且不一定是 SOA 意义上的自治服务。相反,它们与承载自身的平台和域相绑定。基于这一点,它们几乎不能称为 WS-* Web 服务或 SOA 服务。
此外,这些服务是不使用公共文档资料或发现架构的范例,这一点与 WS-* Web 服务的 Web 服务描述语言 (WSDL) 这类事物不同。这会减少依附于服务的依赖关系的数量,并使服务代码更加迅速地演变。总而言之,为 AJAX 服务推荐的模式不像 Web 和 SOA 服务背后的模式那样雄心勃勃,但对于将要使用它的环境来说,它依然十分有效。
注意,在本专栏的其余部分,我将使用“AJAX 服务”的说法表示通过脚本服务方法实现 AJAX 应用程序后端的服务。
AJAX 服务返回什么?
既然公开 AJAX 服务的唯一方式是通过 HTTP,那么就几乎可以使用任何文本格式来包装请求和响应的主体。JavaScript Object Notation (JSON) 是最常用的格式,但也可使用其他格式,如纯 XML 和原始文本。
JSON 是基于文本的格式,用于跨应用程序的各层移动对象的状态。JSON 字符串通过常见的 eval 函数便可方便地赋给 JavaScript 对象。JSON 格式描述了对象,如下所示:
{"ID":"ALFKI", "Company":"Alfred Futterkiste"}
该字符串表示一个对象有两个属性,即 ID 和 Company,以及它们各自的文本序列化值。如果对某个属性赋予非基本类型的值(比如自定义对象),那么该值将递归地序列化为 JSON,如下所示:
"ID":"ALFKI",
"Company":"Alfreds Futterkiste",
"Location":
"{"City":"Berlin", "Country":"Germany"}",
}
使用 eval 函数进行处理时,JSON 字符串将变成一个关联性数组(即一种名称/值的集合),其中每个条目都有一个名称和值。如果 JSON 字符串用于代表一个自定义对象(比如 Customer)的状态,那么,您必须负责确保客户端具有相应类的定义。换句话说,JavaScript 的 eval 函数只是将 JSON 字符串中的信息提取到一个通用容器。如果您需要将此信息公开为一个自定义对象(比如 Customer 对象),那么提供类定义并将数据载入到其中的任务就完全依靠您或您使用的框架来完成。有关 JSON 语法和用途的更多信息,请参阅 www.json.org。
JSON 与 XML 之比较
多年以来,XML 已被推崇为 Web 通用语言。我们知道,现在 AJAX 正在改变着 Web,就数据表示而言,JSON 更受欢迎,而 XML 正在被推向角落。
JSON 稍微简单些,更适合与 JavaScript 语言配合使用。您可能会争辩哪一个更容易为人们所理解,不过,Web 浏览器处理 JSON 肯定比处理 XML 更容易。使用 JSON 时,您不需要 XML 分析器之类的任何东西。为分析文本所需的一切都已经完全构建在 JavaScript 语言中了。因为 JSON 不像 XML 那样雄心勃勃,它也不那么冗长繁杂。这并不是说 JSON 就很完美;JSON 需要大量的逗号和引号,这使它的格式显得十分怪异。
凭借 JSON,您还能以相对较低的成本在体系结构方面嬴得关键优势。您按照对象无处不在的思路进行推理。在服务器上可定义一些实体,并用您最喜爱的托管语言将它们实现为类。当某个服务方法需要返回任何类的一个实例时,该对象的状态被序列化为 JSON 并通过线路传送。客户端接收并处理 JSON 字符串,并将其内容载入一个数组或一种与服务器类有相同接口的镜像 JavaScript 对象。类的接口是从 JSON 流推断出来的。这样,服务和客户端页面的代码便使用一个实体的同一逻辑定义。
单纯从技术角度来说,AJAX 服务并不严格要求 JSON 实现为数据表示格式。使用 XML 也可实现同样的结果,但随后就需要可从 JavaScript 使用的 XML 分析器。在 JavaScript 中分析某些简单的 XML 可能不成问题,但使用一个完备的分析器就是另外一种情形了。性能和功能相关的问题将可能导致存在大量表面上相似而实际上共同点很少的组件。随之而来的问题就是 JavaScript XML 分析器是否支持命名空间、架构、空格、注释、处理指导等。
依我看,出于兼容性的考虑,最终的产物将是 XML 的一个子集,仅限于节点和属性。那样的话,这就只是在尖括号的 XML 和花括号的 JSON 之间作出选择的问题了。
安全性考虑
AJAX 提供动态性更强、交互性更强的浏览体验。不过,这增加了跨站点脚本 (XSS) 和跨站点请求伪造 (CSRF) 等常见类型攻击的可能性。这些类型的攻击是由攻击者向 Web 页面注入脚本代码而引发的,一般是通过 URL 实施,从而使攻击者能够控制 Web 浏览器,执行某些操作,如窃取用户名和密码,或者在用户不知情的情况下执行 HTTP 请求。
例如,攻击者可以使用动态创建的"SCRIPT"标记将恶意脚本注入客户端,从而允许将数据导入到攻击者的网站。在实施 CSRF 攻击时,攻击者可以将一段脚本注入客户端,通过使用客户端上保存的身份验证信息(比如 cookie),在另一个网站上执行未经授权的服务方法。
JSON 是一种包装数据并将其传送给客户端的有效方式。不过,由于 JSON 被认为是 JavaScript 语言的一个安全子集(它不包括赋值和调用操作),因此许多 AJAX 应用程序只是简单地将 JSON 字符串直接传递给 JavaScript 的 eval 函数来创建 JavaScript 对象。因此,非正常的 JSON 字符串为攻击者提供了在客户端执行未授权脚本的新手段。
这些攻击类型中的大多数都依赖于利用 HTTP 请求上的 GET 动词。庆幸的是,ASP.NET AJAX 服务在默认情况下对远程服务禁用了 GET 动词。另外,您可以要求在请求中设置一种特殊的内容类型:其他任何情况下,ASP.NET AJAX 服务将拒绝该调用。通过设计,任何从 "SCRIPT"标记发出的对 ASP.NET AJAX 服务的直接调用都具有错误的内容类型,因而不会成功。
ASP.NET AJAX 中的服务
通过 ASP.NET AJAX Extensions 实现脚本服务有两种方式——使用特殊类型的 ASP.NET Web 服务和通过页面方法。前一种情况下,您只需设计和构建一个链接到 ASMX 资源的类:
<%@ WebService Language="C#"
CodeBehind="~/App_Code/TimeService.cs"
Class="IntroAjax.WebServices.TimeService" %>
该类可以选择从 WebService 类继承,并且必须用新的 ScriptService 属性加以修饰:
[ScriptService]
public class TimeService : System.Web.Services.WebService
{
...
}
每个可调用脚本的方法均声明为公共方法,并标记有通常的 WebMethod 属性。
页面方法只不过是在单个 ASP.NET 页面上下文中定义的公共、静态方法,每个方法都标记为 WebMethod。只能从宿主页面中调用它们。除了存储不同之外,对 Web 服务或页面方式的调用均由 ASP.NET AJAX 环境以相同方式进行处理。
您必须清楚,AJAX 服务代表后端的一部分。从 WS-* Web 服务意义上讲,它们并不是公共 Web 服务,Web 服务都是通过 WSDL 脚本完整记录并可通过携带 SOAP 数据的 POST 命令进行访问。AJAX 服务是真正的本地服务,通常在调用它们的同一个应用程序中进行定义。然而,它们也可以在不同的 Web 应用程序甚至不同的网站上,前提是它们位于同一个域。
通过启用 ASP.NET AJAX 运行时以接受对服务的调用,ScriptService 属性扮演了一个关键角色。如果没有 ScriptService 属性,当您试图进行调用时,服务器上就会引发异常。图 2 显示当一个 AJAX 页面链接到未标记该属性的服务时所返回的消息。注意,图中显示的页面永远不会显示给任何用户。当连接到声明使用此类服务的 ASP.NET AJAX 页面时,您确实会收到全部该标记。该标记返回 HTTP 500 错误代码,因为 ASP.NET 内部机制拒绝处理缺少该属性的 ASP.NET Web 服务进行的脚本调用。
图 2 引用不可脚本化服务的页面
图 2 引用不可脚本化服务的页面
默认情况下,AJAX 服务方法只能使用 POST 动词调用并返回 JSON 格式的数据。然而,通过使用各服务方法中可选择的 ScriptMethod 属性,可以更改这些设置。图 3 详细说明了 ScriptMethod 属性所支持的参数。返回到客户端的数据可以更改为 XML,甚至可以添加对 GET 请求的支持。但我已说过,这种表面看起来无恶意的更改可能会为攻击者提供新的机会,且增加了跨站点调用该方法的可能性。下列代码段显示了一个 Web 服务方法的定义:
[WebMethod]
[ScriptMethod]
public DateTime GetTime()
{
...
}
如果您不准备更改任何默认设置(多数情况下我建议这样做),则可以忽略 ScriptMethod 属性。不过,WebMethod 属性是必选项。
ASP.NET AJAX 服务和 SOAP
一旦创建了 AJAX Web 服务,它就发布为 ASMX 资源。默认情况下,它是公共 URL,可以由 AJAX 客户端使用,也可以由 SOAP 客户端及工具发现和使用。不过,您可以选择同时禁用 SOAP 客户端及工具。只需在承载服务的 ASP.NET 应用程序的 web.config 文件中输入下列配置设置即可:
这个简单的设置禁用了为 ASP.NET 2.0 Web 服务定义的任何协议(特别是 SOAP),并让服务仅回复 JSON 请求。注意,当这些设置开启时,您无法再通过浏览器的地址栏调用 Web 服务来进行快速测试。同样,您也不能请求 WSDL 向 URL 中添加 ?wsdl 后缀。
为了在 Web 应用程序中启用 ASP.NET AJAX,必须在 web.config 文件中包含下列设置:
节点会丢弃 ASMX 资源的默认 HTTP 处理程序,即通过 SOAP 处理请求的处理程序。 节点会添加一个新的 HTTP 处理程序,该处理程序主要检查每个传入的 ASMX 请求的内容类型,如果内容类型头设置为 application/json,则通过 JSON 处理该请求。否则,HTTP 处理程序假定该请求是基于 SOAP 的,并将其转发给标准的 ASP.NET 2.0 Web 服务处理程序。如果禁用了 SOAP 协议,则会拒绝该请求。
最后,不管表面如何,没有任何必要让 SOAP 介入 ASP.NET AJAX 服务。但对 SOAP 客户端的支持是得到保证的,除非在 web.config 文件中明确禁用。
要使 ASP.NET AJAX 服务按预期正常工作,传入请求的内容类型 HTTP 头必须设置为 application/json。对于通过"SCRIPT"标记实施的跨站点攻击来说,这还是极好的补救措施。
调用 AJAX 服务
要调用 AJAX 服务,AJAX 客户端会遵从在 Windows 和传统的 ASP.NET 应用程序中引用 Web 服务的相同模式。一个代理类在本地提供与远程服务相同的接口。在 ASP.NET AJAX 应用程序中,此代理是一个 JavaScript 类,当页面下载后,由运行库生成。
JavaScript 代理类具有与脚本服务相同的名称和许多附加属性。它的特征是有一组相同的方法,只不过采用了稍有扩展的签名。一般而言,您没有必要探究代理类的源代码。不过,如果您想看一下它的结构,则可从浏览器的地址栏中尝试调用下列 URL:
http://.../service.asmx/js
浏览器将下载一个 JavaScript 文件,您可以将其保存到本地磁盘,供以后详阅。
JavaScript 代理类从一个名为 Sys.Net.WebServiceProxy 的基类继承而来。它提供了进行 JSON 调用的基本功能。本专栏的代码下载提供一个 Web 服务的代理类,它具有下列接口:
interface ITimeService
{
DateTime GetTime();
string GetTimeFormat(string format);
}
JavaScript 代理类的特征是具有图中列出的属性。除了常规的一组参数之外,每个镜像的方法还有三个参数。这三个参数分别是:方法成功时所要调用的回调函数、方法失败或超时情况下所要调用的回调函数、传递给两个回调的上下文对象。通过图中显示的三个与默认相关的属性,您可以对多次调用重复使用同一个函数(比如处理错误的唯一 JavaScript 函数)。下面是从 ASP.NET AJAX 页面调用一个远程 AJAX 服务的一些示例代码:
function getTime()
{
IntroAjax.WebServices.TimeService.GetTimeFormat(
"ddd, dd MMMM yyyy [hh:mm:ss]", onMethodComplete);
}
function onMethodComplete(results)
{
$get("Label1").innerHTML = results;
}
在方法调用结尾(无论结果如何)调用的回调具有下列原型:
function method(results, context, methodName)
context 参数代表调用时指定的上下文对象。methodName 参数是一个设为服务方法名称的字符串。最后,对于成功调用时所调用的回调,results 参数是包含 JavaScript 版本的方法返回值的对象。对于失败的回调,此参数则代表 Sys.Net.WebServiceError 对象。
构建用户界面
AJAX 完全是关于最广泛意义上的用户体验——连贯的感受、无闪烁更新、界面设施、资源聚合、实时数据等等。但您只能利用浏览器和它的一套可编程性功能,主要是浏览器的对象模型、DOM 实现、对 DHTML 扩展的支持、CSS、JavaScript 和插件。
JavaScript 是构建和操纵 UI 的主要工具。用户界面任务的典型模式要求客户端使用 JavaScript 调用远程服务、接收 JSON 数据或可能是 XML 数据,然后重新整理页面以显示更改。
如此简单的模型在应用到实际应用程序的规模和复杂性时未必有效。随着 UI 的结构变得日益复杂化,重新整理页面来合并远程调用后新来的数据所带来的问题不可小觑。关键是,一个复杂的 UI 成为问题的临界点在哪里?
从根本上说,每个实际应用程序(尤其是业务线应用程序)必须依赖三个基本的用户界面功能:布局、数据绑定和样式。除了通过 CSS 支持某些样式外,JavaScript 环境不支持任何这些用户界面功能。此外,在大量的编程常因浏览器的细微错误和粗劣的编程而导致内存问题之类的情况下,JavaScript 还是一种解释性语言。
考虑一个比较常见的简单情形——对数据网格分页。网格归根到底就是十分复杂的表,浏览器必须为每个请求分析和呈现它。第一次处理页面时,用户很难衡量此呈现操作的成本,因为它已合并到整体页面下载过程中。但是,一旦对新网格页面提出 AJAX 回发请求,更新浏览器窗口的成本就会立即显现。浏览器收到包含对象集合的 JSON 字符串,而且必须将该集合转换成新的表。在客户端必须构建庞大的 HTML 字符串,在一些内存缓冲区中组织文本。如果没有差错,相同的字符串随后还必须以图形方式呈现。
当完成如此密集的操作后,您会发现响应时间并非如此理想。对于类似上述的情形,要找出有效的解决方案,需要具备中等以上的 JavaScript 和 DHTML 技能。
一个替代方法是在服务器上预先生成一些标记。这样,远程服务不仅返回数据,而且还合并标记信息。在服务器上,标记是通过已编译的代码来构建的,因此可以更容易地调试和测试它,而且您可以用更强大的编程工具来添加可访问性功能。另一方面,线路上将传输更多的数据(但是请记住,数据量仍远少于常规 ASP.NET 回发)。
底线就是,无论是从性能还是从开发角度考虑,为了有效地创建切实可行的用户界面,客户端需要更强大的工具。完备的部件和控件库对任何开发人员来说都十分必要。但是,即使经过最完美优化的库对于打破解释性语言(比如 JavaScript)的固有限制也无能为力。也许 Silverlight 最终会向 Web 开发人员提供他们所希望的用于下一代 Web 和 AJAX 应用程序的客户端环境;Silverlight 是一个 Microsoft 跨平台浏览器插件,它结合了 Windows®
Presentation Foundation (WPF) 框架的一个子集。但是 Silverlight 是一个外部插件,需要单独下载,而且尚未成熟。1.0 版刚发布不久,若要用它构建真正实用的 Web 应用程序的表示层,还需要一些更高级的功能。
这一切意味着什么
部分呈现是实现 AJAX 最简便的方法。它非常适合于将 AJAX 功能添加到旧式应用程序中,您不必花时间、预算来重新设计这些应用程序,也不会有重新进行设计的愿望。从体系结构的角度上说,部分呈现是对当今 ASP.NET 的智能化扩展,而且保留了相同的应用程序模型和底层引擎。
纯 AJAX 体系结构是基于客户端与服务器的松散耦合,即实质上彼此独立的两个世界通过 HTTP 线路就 JSON 交换消息连接了起来。在纯 AJAX 体系结构中有一个基于服务的后端和一个 JavaScript 驱动的前端。构建有效的 HTML UI 完全取决于您或您选择的控件库。不过,这种分离机制使 Web 开发人员能够继续采用 Silverlight 等新兴技术创建交互性更强的用户界面,而不受服务器平台的限制。
Dino Esposito是 Solid Quality Learning 的一位导师,也是即将出版的“Programming ASP.NET 3.5 Core Reference”(ASP.NET 3.5 技术内幕)(Microsoft Press, 2007) 一书的作者。Dino 定居于意大利,并经常在世界各地的业内活动中发表演讲。您可访问他的博客,网址为 weblogs.asp.net/despos。