深入了解 Microsoft AJAX Library
Dino Esposito
代码下载位置: CuttingEdge2007_12.exe (152 KB)
Browse the Code Online
目录
AJAX 是异步 JavaScript 和 XML 的缩写,暗示着 AJAX 和 JavaScript 之间有着必然的联系。尽管 JavaScript 是简单的编程语言,但它早在多年前就已经打下了坚实的基础,虽然当时人们认为对网页而言面向对象的编程是多余的,而且动态语言的原则也太过理论化,无法在已广泛使用的编程语言中完全实现。
因此,JavaScript 的覆盖面相当广:它支持部分面向对象的语义,其方式与更加完善的面向对象的语言(如 C#)类似,并且它还包含一些动态绑定功能,类似于现在的 Ruby 或 Python。但是,JavaScript 是无数网页使用的一种语言,用于使页面更具交互性,而不是纯粹的文本、表格和图形的静态组合。
我们所熟知的 JavaScript 实际上是浏览器的标准化 ECMAScript 语言实现。使用该语言主要是为了将函数嵌入 HTML 页面中,从而将用户的操作与页面中的元素结合在一起。页面中的元素(按钮、文本框等等)都是以文档对象模型 (DOM) 中定义的架构为模型。使用 JavaScript,您可以编程方式重命名、编辑、添加或删除显示文档中的元素,以及处理由此类元素触发的任何事件。此外,还可以执行任何特定于浏览器的操作,如打开或弹出新的窗口,或调用常用的 XMLHttpRequest 对象并对远程 URL 发出异步调用(AJAX 由此而变得特殊)。
需要记住的关键一点就是 JavaScript 和 DOM 本身不再为开发人员提供 Web 现阶段需要的全部编程功能。它们代表一种可以使用的最小工具箱。为了构建优秀的 AJAX 页面,还需要更多东西,例如丰富的客户端 JavaScript 库。
AJAX 的一个很大优点就是能在客户端上运行站点的更多代码,使浏览器能够对用户操作做出快速响应。不过,典型的 AJAX 操作不仅当鼠标在元素上滚动时简单切换图像,或者显示带有菜单和工具栏的弹出窗口,还包括客户端数据验证、高级用户界面功能,如浮动菜单、拖放、弹出面板及复杂输入。此外,AJAX 操作还能够以带外方式将请求发送给服务器,该方式绕过了整页替换的传统模型。
无论您考虑使用何种 AJAX 平台,都必须有丰富的 JavaScript 库支持。这些库通常至少包含一*成的客户端用户界面小工具和一个带有为每个已加载文档定义生命周期的事件的应用程序模型。在 ASP.NET AJAX 1.0 和集成了 ASP.NET 3.5 的 AJAX 平台中,JavaScript 库的最佳选择是 Microsoft
® AJAX Library。
Microsoft AJAX Library 简介
Microsoft AJAX Library 由两个相关的部分组成:JavaScript 语言扩展和一种基类库,可为开发人员提供预定义的服务和工具。尽管对象的概念已深入人心,但是 JavaScript 本身不能作为面向对象的语言,因为它无法在本机全面实现面向对象编程的三个支柱:继承、多态性和封装。通过对象原型可以获得少许继承,通过闭包也可以获得少许封装。因此 Microsoft AJAX Library 会先为语言提供更多强大的工具,然后再继续定义新的类和编程工具。
Microsoft AJAX Library 本身是独立的,可用 JavaScript 进行编写,并存储在几个 .js 文件中。这意味着只要正确引用了构成文件,任何接受 JavaScript 的编程环境都可以成功地使用 Microsoft AJAX Library。Microsoft AJAX Library 中包括两个主要的文件:MicrosoftAjax.js 和 MicrosoftAjaxWebForms.js。MicrosoftAjax.js 定义 Microsoft AJAX Library 支持的语言扩展,包括命名空间、接口、枚举和继承。MicrosoftAjaxWebForms.js 则定义部分呈现引擎和整个网络堆栈。
在 ASP.NET AJAX 应用程序中,无需直接引用或嵌入这两个文件中的任何一个。这是 ScriptManager 控件为您完成的任务之一:
<asp:ScriptManager runat="server" ID="ScriptManager1" />
置于 ASP.NET AJAX 页面中之后,上述代码保证 Microsoft AJAX Library .js 文件中的一个或两个都会被下载到客户端。如果要在非 ASP.NET AJAX 应用程序的上下文中扩展浏览器 JavaScript 的功能,则必须显式引用所有必需的 .js 文件:
<head> <script src="microsoftajax.js"></script> </head>
Microsoft AJAX Library 的源代码位于
ajax.asp.net/downloads。您可以在自己的页面中修改这些脚本。
基于 Microsoft AJAX Library 的类
在传统的 JavaScript 编程中,可通过创建函数来进行编程。由于函数是一级元素,因此可以创建函数的新实例,并对其进行配置以供执行。换句话说,函数的用法基本上就如同它们是面向对象语言中的对象一样。这是 JavaScript 体系结构的支柱,一旦更改就有破坏许多应用程序和大量现有浏览器的严重风险。Microsoft AJAX Library 运用了一些技巧,在使 JavaScript 函数的外观和行为类似于传统对象的最终目标方面做了一些假设。
使用原型模型可以创建 Microsoft AJAX Library 类。所有 JavaScript 函数都具有原型属性的特点。原型属性的目的是代表该对象内部尚未公开的架构。如果将某个方法定义为函数原型的组成部分,则该函数的所有实例最终都将调用同一方法实例。
例如,假设有一个 Timer 对象:
MsdnMag.Timer = new function() { // This is a sort of constructor // for the function : this._interval = 5000; }
根据 Microsoft AJAX Library,这是类的定义。可是,您可以看到,它只是一个函数定义。通过上述代码,您定义了可以在链接该代码的任何页面中进一步实例化的函数对象:
var timer = new MsdnMag.Timer();
伪构造函数中定义的任何代码都可在此时执行。根据上述代码段,实例化函数会将内部的 variable _interval 设为 5000。
怎样才能将一些开始和停止方法添加到该计时器对象上去呢?原型模型指定了要使用若干独立的代码块来定义类。在面向对象的编程语言(如 C#)中,类的所有代码都包含在单个的类定义中(显然分部类除外)。要将方法添加到计时器对象,需要无缝添加无关的代码段,如下所示:
MsdnMag.Timer = new function() { this._interval = 5000; this._timerID = null; } MsdnMag.T imer.prototype
.start = function(callback) { this._timerID = window.setTimeout(callback, this._interval); }
让我们进一步看一下这个代码片段,因为它包含了一些有关 Microsoft AJAX Library 编程的重要线索。首先,将此代码和一些等效的 C# 代码进行比较:
namespace MsdnMag { public class Timer { public Timer() { _interval = 5000; _timerID = null; } protected int _interval; protected object _timerID; public void start(Delegate callback) { // Any code that can create a timer _timerID = CreateTimer(callback, _interval); } } }
首先您会注意到,不需要将 _timerID 和 _interval 显式定义为 Microsoft AJAX Library 中 Timer 对象的组成部分。只要在构造函数中为两个成员指定默认值即可。然而,为了将它们标记为对象的成员,必须将关键字“this”作为其前缀。否则,它们会被认为是该函数的专有成员,且没有绑定到对象的原型上。如果将关键字“this”作为前缀,则可以从任何原型方法调用这些成员。
正如本专栏定义的那样,MsdnMag.Timer 函数阐明了在 ASP.NET AJAX 和任何其他使用 Microsoft AJAX Library 的平台中的 Microsoft AJAX Library 之上构建新类的推荐方法。请查看
图 1,并将它与上述代码段进行比较。
Figure 1 MsdnMag.Timer 类的闭包
此处 JavaScript 对象已编写成闭包,而不是原型对象。大多数浏览器提供的 JavaScript 引擎都支持这两个模型。那么它们有什么区别呢?闭包模型提供了一个对象,该对象的所有成员都在同一个整体上下文中定义,这与 C# 类十分类似。该原型模型提供了具有一种分布式架构的类。附加到该对象原型上的所有成员都为该对象的所有实例所共享。Microsoft AJAX Library 完全根据原型模型编写,Microsoft 强烈建议为您自己的对象使用该模型。另一方面,Microsoft AJAX Library 正好是 JavaScript 之上的抽象层,而 JavaScript 既支持闭包,也支持原型。因此,您可以在这些编程风格之间进行选择。
那么使用 Microsoft AJAX Library 有什么好处?该库通过添加多个提供新语言功能(如命名空间、枚举类型、委托、更强大的类型化和继承)的预定义对象扩展了 JavaScript 编程环境。
图 2 显示了作为完全限定的 Microsoft AJAX Library 对象编写的 MsdnMag.Timer 对象。示例计时器类在构造函数中最多接受三个参数,从而初始化该计时器的间隔和回调以定期运行。该计时器会自动重新启动,直到显式停止它并提供了回调重置为止。
Figure 2 MsdnMag.Timer 基于原型的版本
请注意,如果在 JavaScript 函数调用中省略了声明的参数,则正式参数的实际值会计算得出未定义的类型。因此,应该检查一下是否有空值和未定义的类型。以下代码显示了检查 JavaScript 参数是否为空的一个安全方法:
if (typeof(this._resetCallback) !== "undefined" && this._resetCallback !== null) { ... }
如果想将公共的读取/写入属性添加到新对象中,该怎么办?在 JavaScript 中,您无法通过技巧利用基于属性的编程接口使具有 get 访问器或 set 修饰符的属性变为可用。只要想让它成为属性,而且不仅仅是类似于字段的公共数据容器,就需要使用特殊方法读取和写入属性值。通常使用的是 get_XXX 和 set_XXX,其中 XXX 是属性的名称。MsdnMag.Timer 对象上的 Interval 属性的示例实现如
图 3 所示。
Figure 3 将属性添加到 MsdnMag.Timer
在 get 访问器中,可检查传递参数的数量,并引发参数计数错误(如果有所指定)。请注意,“参数”是 JavaScript 的内部数组,返回用于函数调用的参数列表。
Microsoft AJAX Library Function 对象上的 _validateParams 方法是一个扩展,它使开发人员检查类型和方法参数数量变得非常简单:
var e = Function._validateParams( arguments, [{name: 'value', type: Number}]);
该函数采用了参数列表和第二个数组来说明预期的参数列表。在上述代码段中,该方法应只接收一个含正式值名称的数字参数。_validateParams 方法会验证该参数数组只包含一个数字元素,否则将引发异常。
Microsoft AJAX Library 中的内置类
现在您已了解如何在 JavaScript 中使用 Microsoft AJAX Library 框架作为基础来创建新类,那么就让我们研究一下内置在该框架中的系统类吧!
图 4 介绍了在 Microsoft AJAX Library 中定义的所有 JavaScript 类。固有的 JavaScript 对象(如 Boolean、Date 和 Number)已得到扩展,包括了新的方法和功能。例如,Date 对象现在包括多个名为 localeFormat 的新实例方法和格式。您可以通过使用区域设置文化信息来使用它们呈现日期:
Figure 4 Microsoft AJAX Library 内置类
var d = new Date(); d.localeFormat("dddd, dd MMMM yyyy, hh:mm:ss");
使用下列代码即可获取有关当前区域设置的信息:
var localShortDate = Sys.CultureInfo.CurrentCulture.dateTimeFormat.ShortDatePattern; var d = new Date(); d.localeFormat(localShortDate);
特别是,此代码段会根据当前文化检索短日期模式,并使用它来设置日期格式。
Sys.CultureInfo 对象在何处检索区域设置信息呢?只要 EnableGlobalization 属性在脚本管理器中设置为 true,有关当前文化的信息就会在客户端页面中发布:
<asp:ScriptManager ID="ScriptManager1" runat="server" EnableScriptGlobalization="true" />
脚本管理器会编写出一段 JavaScript 代码,然后再使用 JavaScriptSerializer 类来分析这段代码,并将它与 Microsoft AJAX Library 运行库集成。Date 对象也提供特定于文化的方法,以尝试从任意文本构建新的日期对象。
Number 对象也有相似的功能,用于特定于文化的分析和格式化。
Boolean 对象是另一个静态分析方法,可识别类似于“true”和“false”的单词。
Array 和 String 均属于在 Microsoft AJAX Library 中得到大量扩展的其他固有类型。特别是,Array 类型提供了很多新的静态方法,可以执行添加、删除、搜索和遍历项目等操作。通过添加 endsWith、startsWith、裁减、格式化以及与字符串生成器集成之类的方法,创建的 String 类型已经尽可能地与托管 String 类接近。
现在,所有对象都返回精确的类型信息。不论您查询什么对象,其类型方法都能够立即告诉您正确的类型。操作方法如下所示:
myTimer = new MsdnMag.Timer(2000, updateUI, timerReset); alert(Object.getTypeName(myTimer));
图 5 显示了警告函数的效果。
图 5
使用 AJAX 检查类型信息 (单击该图像获得较大视图)
在所有本机 JavaScript 对象中,只有在正则表达式内置支持背后的 RegExp 对象没有在 Microsoft AJAX Library 中以任何方式进行扩展。一旦安装了这个库,要使用的 RegExp 对象就会和 Microsoft AJAX Library 出现之前完全一样。
XMLHttpRequest 在哪?
尽管旨在提供更丰富且更加面向对象的 JavaScript 编程环境,但 Microsoft AJAX Library 却更多地被视为 AJAX 库。因此会出现一个有趣的问题,那就是 XMLHttpRequest 在哪?如果您仔细观察一下 Microsoft AJAX Library 类的层次结构,就会发现 XMLHttpRequest 隐藏在 Sys.Net 命名空间的类的网络堆栈中。
在 Microsoft AJAX Library 中,所有的 Web 请求都通过 Web 请求管理器对象传递。Sys.Net_WebRequestManager 对象的唯一实例在加载时创建,并在页面的整个生存期内使用。虽然是异步触发请求,但是管理器不能同时接受多个请求。尽管这在网页中不可能,但是如果同时要求请求管理器的同一个实例来处理多个请求,那么第二个请求会引发异常。开发人员的责任是在稍后计划第二次尝试。
Web 请求管理器维护事件处理程序的列表,并定义请求的超时和执行器类型。您可以通过 defaultTimeout 属性设置默认的超时,也可以通过使用 defaultExecutorType 属性(一个字符串)更改默认的执行器类型。在默认情况下,该执行器是 Sys.Net.XMLHttpExecutor 类的实例,并且它在触发请求之前就已实例化。
执行器是实际连接远程 URL 并发送 HTTP 数据包的代码。在 AJAX 中,完成这个任务最简单的方法就是使用浏览器的 XMLHttpRequest 对象。Sys.Net.XMLHttpExecutor 类只封装了 XMLHttpRequest 对象,并使用其服务将 HTTP 数据包发送到它的目的地。
您可以看到,执行器是一个独立模块,管理器只能通过由 executeRequest 方法和 invokingRequest 及 completedRequest 事件组成的已知接口来使用它。让我们深入了解一下 Microsoft AJAX Library 中的 Web 请求机制。
Web 请求的详细情况
Sys.Net.WebRequest 类定义发送到服务器 URL 以进行处理的 HTTP 数据包。该对象的公共属性是 url、httpVerb、标头、正文、超时、userContext 和执行器。如未设置,超时和执行器将默认为在 Web 请求管理器中定义的值。已完成事件和调用方法是 WebRequest 对象编程接口的剩余部分。要触发请求,须调用该调用方法。您可以看到,一次只支持一个请求,并且这个请求会被传递到请求管理器中以待执行:
function Sys$Net$WebRequest$invoke() { if (arguments.length !== 0) throw Error.parameterCount(); if (this._invokeCalled) { throw Error.invalidOperation(Sys.Res.invokeCalledTwice); } Sys.Net.WebRequestManager.executeRequest(this); this._invokeCalled = true; }
请求管理器会检查是否对数据包中的执行器有专门的要求。如果没有,则使用默认的执行器。接着,请求管理器会触发 invokingRequest 方法。如果事件没有被用户代码取消,则执行最后一个步骤:该执行器实际设置连接,并发送数据包。
该执行器从名为 WebRequestExecutor 的基类派生而来,后者定义了预期的编程接口。如前文所述,默认的执行器是 XMLHttpExecutor。这个类在内部创建了 XMLHttpRequest 浏览器对象的实例,并用它来传送请求。
可以为每个 Web 请求指定另一个执行器,因为它是 Sys.Net.WebRequest 对象的属性。虽然在 Microsoft AJAX Library 中只定义了一个实际的执行器(即 XMLHttpExecutor),但是开发人员也可以创建其他执行器。关键是,有什么其他技术和替代方案可以作为创建自定义执行器的理由呢?
额外的执行器可以使用动态创建、不可见的 IFRAME 来下载任何 URL 发回的所有响应。您会注意到,使用 XMLHttpRequest 时,您局限于大多数浏览器都实现的所谓“同源策略”。实际上,这意味着浏览器不允许以脚本方式调用位于当前页面域之外的 URL。比如说,使用 XMLHttpRequest 时,您不能从 contoso2.com 调用到 contoso1.com。但可以用另一个方法完成此任务,即使用 IFRAME 获取响应,然后将其解析为任何适当的 JavaScript 对象。
要编写基于 IFRAME 的执行器,您应该从 Microsoft AJAX Library 对象开始着手,该对象从 Sys.Net.WebRequestExecutor 继承而来,并实现了其所有抽象成员。由于将响应分析成可用的内存对象,您可能需要定义一个特定于请求的分析器组件,该组件在常见的编程接口下提供自定义行为。
脚本服务的代理对象
可编写脚本的服务需要一些 JavaScript 包装代码,以便调用。因此,可以生成基于脚本的代理类,它提供的公共编程接口几乎和原始服务(不管是传统的 ASP.NET Web 服务还是 Windows Communication Foundation (WCF) 服务)的公共编程接口一模一样。代理类通过绑定到特定 URL 的脚本标记链接到页面:
http://www.yourserver.com/service.asmx/js
代理对象的名称和服务类完全限定的名称相同。该对象还具有一组相同的方法。可是,这些方法都有一个扩展签名,该签名除了包含一组标准的输入参数以外,还包含其他参数。特别是,每个方法都添加了两个回调函数以允许用户代码处理成功或失败的调用,以及代表调用上下文的可选对象。该代理对象由 Sys.Net.WebServiceProxy 的 Microsoft AJAX Library 类派生。
图 6 显示了服务代理类的片段。出于性能方面的原因,代理类是在回调模型中异步执行其每个方法的单一实例。在这个代码中,_invoke 方法最终创建 Web 请求(这一点很重要),并且还将内容类型设置为 application/json,这是 ASP.NET AJAX 运行时必需的设置。示例片段:
Figure 6 Web 服务代理类
var request = new Sys.Net.WebRequest(); request.get_headers()['Content-Type'] = 'application/json; charset=utf-8';
超时可通过内部计时器进行自动设置和控制。此外,代理为 Web 请求的已完成事件注册了处理程序。当事件触发时,系统提供的代码便会检查响应。如果一切正常,该代理会调用用户定义的成功回调。否则调用失败回调,并向失败回调传递有关发生的错误的信息,如以下段所示:
var error = response.getResponseHeader("jsonerror"); var errorObj = (error === "true"); if (errorObj) err = new Sys.Net.WebServiceError(false, result.Message, result.StackTrace, result.ExceptionType); ... onFailure(err, userContext, methodName);
如果执行该服务方法会导致未处理的异常,那么与此异常相关的消息、其堆栈跟踪,以及它的类型都会传回客户端,并组合到失败回调可以使用的 WebServiceError JavaScript 对象中。
部分呈现引擎
在 Sys.WebForms 命名空间中,部分呈现的关键对象是 PageRequestManager。初始化该对象的代码可以透明地插入至少包含一个 UpdatePanel 控件的任何客户端页面。如果您查看了此类页面的源代码,就会发现有一段脚本如下所示:
Sys.WebForms.PageRequestManager._initialize( 'ScriptManager1', document.getElementById('form1'));
ScriptManager 控件的 ID 以及表单的 ID 都具有任意性。PageRequestManager 上的 _initialize 方法负责执行两个任务。第一,创建该对象的唯一实例以满足页面生命周期中的所有需要。第二,为表单的提交事件注册脚本处理程序。这样,您最终就有了截取任何类型回发(通过浏览器完成的常规回发、跨页面的回发,以及通过 DOM 下达的回发)的共用代码段。
提交处理程序的目的是停止浏览器的默认操作,并用通过默认 Web 请求执行器托管的 AJAX 请求来替换表单提交。
提交处理程序首先会创建请求的主体。方法是循环遍历表单中的输入字段,并创建正确的字符串。除了一个额外的参数以外,该主体几乎和浏览器本身会生成的主体一模一样:
ScriptManager1=UpdatePanel1
再次重申,这里的这些 ID 都具有任意性。关键是将触发调用的可更新面板的 ID 指定给页面脚本管理器的 ID。
一旦请求准备就绪,PageRequestManager 就会首先触发客户端的 initializeRequest 事件。该事件允许开发人员取消操作。如果操作可以继续,管理器会终止所有挂起的操作(一次只允许一个部分呈现操作处于活动状态),并触发 beginRequest 事件以通知用户该请求即将开始。接着,它会对相应的 WebRequest 对象执行调用。当响应返回后,会对其进行分析以寻找错误,同时,如果一切正常就会触发 pageLoading 事件。
当 pageLoading 事件到达后,响应已经过分析,但尚未与页面的当前 DOM 合并。接下来,与该操作相关的面板会得到刷新,同时来自服务器的其他所有数据都会得到处理,包括数据项和回发脚本。最后,会触发 pageLoaded 事件,然后是 endRequest 事件。每个 PageRequestManager 事件都有自己的类,并且都包含了特定的信息。