3.3 管理客户端脚本
按照前一节的设定,ScriptManager会自动将核心脚本文件(MicrosoftAjax.js和Microsoft- AjaxWebForms.js)发送给客户端。这样,在客户端我们就已经拥有了ASP.NET AJAX的基本框架以及其带来的统一的JavaScript编程接口。
然而,大多数时候我们的页面中还需要引入一些自定义脚本文件,这个工作同样也将由ScriptManager控件完成。
3.3.1 引入程序集中内嵌的脚本资源
前面曾经介绍,ASP.NET AJAX还包含很多客户端脚本文件,其中定义了大量的客户端组件,将提供拖放、动画等一些非常强大且实用的功能。为了减少不必要的网络流量,默认情况下ScriptManager将不会发送这些文件。若在某些页面中需要这些文件提供的功能,则要在ScriptManager中显式声明。例如,若我们在某个页面中想要使用ASP.NET AJAX提供的完善的客户端组件,则ScriptManager应该显式地声明对PreviewScript.js文件的引用。这样,ScriptManager的代码应该如下所示:
<asp:ScriptManager ID="ScriptManager1" runat="server">
<Scripts>
<asp:ScriptReference Assembly="Microsoft.Web.Preview" Name="PreviewScript.js" />
</Scripts>
</asp:ScriptManager>
上述代码中的<asp:ScriptManager>标签中定义了一个子标签<Scripts>,其中还定义了一个<asp:ScriptReference>标签,并设定了该标签的Assembly属性为Microsoft. Web.Preview,Name属性为PreviewScript.js。通过这个<asp:ScriptReference>标签以及其Assembly和Name属性,ScriptManager即可将PreviewScript.js文件发送至客户端。
这里我们接触到了<asp:ScriptReference>标签的两个常用属性:Assembly和Name,前者用来指定要引用的脚本所嵌入的程序集的名称,后者用来声明嵌入到程序集中的某个脚本的资源名称。这两个属性通常成对使用,让我们能够将嵌入到程序集中的JavaScript资源引入到页面中。ASP.NET AJAX的Value-add部分的脚本均要通过这种方式添加到页面中。在上面的代码中,可以看到Assembly属性设置为Microsoft.Web.Preview,即ASP.NET AJAX的Value-add部分的程序集名称。
在浏览器中查看该页面的源代码并配合使用HTTP嗅探器,可以发现在页面的另外一部分,即一段XML Script中引用了PreviewScript.js文件,如图3-4中选中部分所示。

图3-4 ScriptManager将PreviewScript.js文件发送至客户端
这样,如果我们需要引用嵌入到自定义的程序集中的JavaScript脚本资源,同样可以用<asp:- ScriptReference>标签的Assembly和Name属性来指定。下面的代码演示了在Script- Manager中引用嵌入到MyAssembly.Ajax程序集中的MyJavaScriptResource.js脚本资源的代码:
<asp:ScriptManager ID="ScriptManager1" runat="server">
<Scripts>
<asp:ScriptReference Assembly="MyAssembly.Ajax"
Name="MyJavaScriptResource.js" />
</Scripts>
</asp:ScriptManager>
3.3.2 引入单独的脚本文件
若某个ASP.NET AJAX页面中需要引入我们自己编写的自定义的脚本文件,则ScriptManager的声明应该如下所示:
<asp:ScriptManager ID="ScriptManager1" runat="server">
<Scripts>
<asp:ScriptReference Path="MyPath/MyCustomScript.js" />
</Scripts>
</asp:ScriptManager>
注意,这里的<asp:ScriptReference>标签中并没有定义Assembly或Name属性,而是定义了Path属性,并指向该自定义脚本文件的路径MyPath/MyCustomScript.js。Path属性同样是<asp:ScriptReference>标签的一个非常重要的属性,通过指定脚本文件的路径,Script- Manager即可将其发送至客户端。
查看该页面在浏览器中的源文件,可以看到MyCustomScript.js文件已经添加到了页面的脚本引用中,见图3-5中的选中部分代码。

图3-5 ScriptManager将自定义脚本文件发送至客户端
3.3.3 引入多个客户端脚本
<Scripts>标签下可以定义多个<asp:ScriptReference>,分别代表不同的脚本。例如,若我们需要将PreviewScript.js和PreviewDragDrop.js这两个Value-add脚本文件均发送至客户端,来支持ASP.NET AJAX XML脚本声明以及拖放功能,并且还需要引入位于MyPath/My CustomScript.js路径的自定义脚本,则可以按照如下所示编写ScriptManager的代码:
<asp:ScriptManager ID="ScriptManager1" runat="server">
<Scripts>
<asp:ScriptReference Assembly="Microsoft.Web.Preview" Name="PreviewScript.js" />
<asp:ScriptReference
Assembly="Microsoft.Web.Preview"
Name="PreviewDragDrop.js"
/>
<asp:ScriptReference Path="MyPath/MyCustomScript.js" />
</Scripts>
</asp:ScriptManager>
3.3.4 Debug和Release版本的客户端脚本
我们对程序的Debug和Release两种编译模式都很熟悉,Debug模式的代码在编译过程中会包含很多方便调试、跟踪的信息,而在Release模式中将移除掉这些信息,以提高程序运行的效率。
在客户端脚本开发中,我们同样要考虑Debug和Release两种编译运行模式对脚本的影响。由于JavaScript将以明文形式发送至客户端,所以往往会在Release版本中对方法名、变量名等进行混淆,以避免核心实现代码被窃取,但这种经过了混淆的代码却非常难以调试。这样在大多数情况下,就造成我们自定义的脚本必须分为Debug和Release两个版本。
ScriptManager控件在设计时自然考虑到了这个需求,它提供了一个名为ScriptMode的枚举型属性,可以指定如下几个值:
Auto(默认):该Web页面自动根据当前的编译模式以及Web.config中的相关设置决定应用脚本的Debug版本还是Release版本。
Debug:使用Debug版本的客户端脚本。
Release:使用Release版本的客户端脚本。
这样,若我们希望强制ScriptManager总是发送Release版本的客户端脚本,可以按照如下代码进行声明,注意其中粗体部分:
<asp:ScriptManager ID="ScriptManager1" ScriptMode="Release" runat="server">
<Scripts>
...
...
</Scripts>
</asp:ScriptManager>
然而某些情况下,我们可能希望某个脚本不使用整个ScriptManager中的全局设置。例如,ScriptManager中指定了使用Release版本的脚本,但我们却希望其中的某一个脚本应用Debug版本来方便调试,这时,我们可以使用<asp:ScriptReference>标签(注意:不是<asp:Script- Manager>标签)中的同名属性——ScriptMode,来覆盖定义在ScriptManager中的设定。类似如下的代码,注意粗体部分:
<asp:ScriptManager ID="ScriptManager1" ScriptMode="Release" runat="server">
<Scripts>
...
<asp:ScriptReference Path="MyPath/MyCustomScript.js" ScriptMode="Debug" />
...
</Scripts>
</asp:ScriptManager>
<asp:ScriptReference>标签的ScriptMode属性同样可选Auto、Release和Debug,但其默认值为Inherit,表示继承ScriptManager中的全局设置。
若是<asp:ScriptReference>标签的ScriptMode属性设置为Auto,那么当引用的是程序集中内嵌的脚本资源(使用Assembly和Name属性)时,等同于设置为Release;而当引用的是一个单独的脚本文件(使用Path属性)时,等同于Inherit。
让我们查看下面这段代码在Debug和Release两种不同编译模式下生成的HTML中对脚本的引用方式,其中粗体部分表示将要尝试的两个可选值,这段代码并不能通过编译:
<asp:ScriptManager ID="ScriptManager1" runat="server">
<Scripts>
<asp:ScriptReference Path="MyPath/MyCustomScript.js"
ScriptMode="Debug|Release" />
</Scripts>
</asp:ScriptManager>
当设定ScriptMode="Release"时,生成HTML的脚本引用部分如图3-6中选中部分所示。

图3-6 ScriptMode="Release"时引用的脚本文件
当设定ScriptMode="Debug"时,生成HTML的脚本引用部分如图3-7中选中部分所示。

图3-7 ScriptMode="Debug"时引用的脚本文件
可以看到,在Release模式下,页面直接引用了MyCustomScript.js文件,而在Debug模式下,页面却引用了MyCustomScript.debug.js文件,即在原脚本名最后加上了.debug后缀。这样,我们就应该遵从这一规则,以“[脚本文件名].debug.js”命名Debug版本的脚本文件,并和Release版本的脚本文件放置于同一目录中,以方便ScriptManager的使用。
需要特别注意的是,若网站部署环境的Machine.config文件中<deployment>元素的retail属性设置为true,那么无论<asp:ScriptManager>和<asp:ScriptReference>中的Script- Mode属性如何设置,ScriptManager都将直接发送Release版本的脚本。
3.3.5 设置脚本的根路径
默认的引入程序集中内嵌的脚本资源方法是通过WebResource.axd文件来访问的,例如对于下面的代码,将生成如图3-8选中部分所示的脚本引用HTML:

图3-8 通过WebResource.axd文件访问嵌入到程序集中的脚本资源
<asp:ScriptManager ID="ScriptManager1" runat="server">
<Scripts>
<asp:ScriptReference Assembly="Microsoft.Web.Preview" Name="PreviewScript.js" />
<asp:ScriptReference Assembly="Microsoft.Web.Preview" Name="PreviewDragDrop.js"/>
</Scripts>
</asp:ScriptManager>
这样客户端每次请求时,WebResource.axd文件都将访问<asp:ScriptReference>标签的Assembly属性所指定的程序集,并动态查找生成Name属性所指定脚本资源,这将导致服务器过多的开销。
幸运的是,ScriptManager在设计时考虑了这个问题,它支持一个名为ScriptPath的属性,作为所有脚本文件的起始根目录(全局路径),我们可以将这些内嵌到程序集中的资源文件抽取出来并放置于该目录下,让页面引用这个静态文件,以提高程序执行的效率。这样,对于上面的这段代码,我们就可以为其指定一个ScriptPath,例如~/Scripts,修改后的ScriptManager代码如下,注意其中粗体部分:
<asp:ScriptManager ID="ScriptManager1" ScriptPath="~/Scripts" runat="server">
<Scripts>
<asp:ScriptReference Assembly="Microsoft.Web.Preview" Name="PreviewScript.js"/>
<asp:ScriptReference Assembly="Microsoft.Web.Preview" Name="PreviewDragDrop.js"/>
</Scripts>
</asp:ScriptManager>
这时生成的脚本引用部分的HTML将如图3-9所示。

图3-9 通过设定ScriptPath属性引用静态的脚本文件
对比图3-8和图3-9可以看到,其中对脚本的引用变成了“ScriptPath+程序集名+版本号+脚本文件名”这样的文件引用形式。因此,我们需要在ScriptPath属性所指定的目录下创建同样的目录结构,并将相应的脚本文件置于其中。
若是<asp:ScriptReference>标签中不是使用Assembly和Name,而是使用Path属性直接引用了某个脚本文件,则设定于ScriptManager控件中的ScriptPath将不会起到任何作用。
同时,<asp:ScriptReference>标签还提供了一个IgnoreScriptPath属性(默认值为false),若该属性值设置为true,则这个引用将忽略ScriptPath的设定,仍然使用默认的WebResource.axd文件访问方式。例如如下的代码,注意其中粗体部分:
<asp:ScriptManager ID="ScriptManager1" ScriptPath="~/Scripts" runat="server">
<Scripts>
<asp:ScriptReference Assembly="Microsoft.Web.Preview" Name="PreviewScript.js"/>
<asp:ScriptReference
Assembly="Microsoft.Web.Preview" IgnoreScriptPath="true"
Name="PreviewDragDrop.js" />
</Scripts>
</asp:ScriptManager>
这段代码生成的脚本引用部分的HTML如图3-10所示,可以看到Microsoft.Web. Resources. ScriptLibrary.PreviewDragDrop.js仍旧将通过访问WebResource.axd从程序集中取得。

图3-10 设定IgnoreScriptPath属性为true将忽略ScriptPath属性的设定
3.3.6 响应解析脚本事件
ScriptManager将在解析每一个脚本时触发ResolveScriptReference事件,我们可以按照如下代码声明该事件的处理函数,注意其中粗体部分:
<asp:ScriptManager ID="ScriptManager1" OnResolveScriptReference="ScriptManager1_
ResolveScriptReference" runat="server">
<Scripts>
<asp:ScriptReference ScriptMode="Debug" Path="MyControls.js" />
</Scripts>
</asp:ScriptManager>
在服务器端ScriptManager1_ResolveScriptReference()函数中,我们可以通过e.Script参数得到目前待解析的这个ScriptReference对象,如图3-11所示,这样即可根据情况对其进行相应的修改配置。
下述代码演示了在ResolveScriptReference事件的处理函数中,根据当前ScriptRefer- ence对象的ScriptMode属性值修改其Path属性,相应地指向Debug\和Release\两个文件夹中不同版本的脚本:
protected void ScriptManager1_ResolveScriptReference(object sender,
ScriptReferenceEventArgs e)
{
if (e.Script.ScriptMode == ScriptMode.Debug)
{
e.Script.Path = "Debug/" + e.Script.Path;
}
else if (e.Script.ScriptMode == ScriptMode.Debug)
{
e.Script.Path = "Release/" + e.Script.Path;
}
}

图3-11 ResolveScriptReference事件处理函数中访问当前待处理的ScriptReference对象
3.3.7 <script>标签在HTML中的位置
ScriptManager控件的LoadScriptsBeforeUI布尔值属性可用来设定ASP.NET AJAX脚本文件在最终HTML页面中出现的位置,其默认值为true。
当LoadScriptsBeforeUI设为true时,脚本文件的引用(即<script>标签)将出现在所有界面HTML元素之前,这样即可保证页面显示时已经加载并初始化了ASP.NET AJAX的客户端框架,用户又可以立即开始使用该程序。
当LoadScriptsBeforeUI设为false时,脚本文件的引用将出现在所有界面HTML元素之后,这样用户将首先看到页面中的各个HTML元素,随后再加载ASP.NET AJAX的客户端框架。虽然这种设定会让用户感觉页面的加载速度有所提升,但之后在等待加载ASP.NET AJAX客户端框架的一段时间内程序仍然不可用。若加载过程需要较长时间,反而会造成一种“假死”的状态,影响程序整体的用户体验。
如果没有特殊需要,我们推荐使用LoadScriptsBeforeUI属性的默认值true。
3.3.8 脚本文件的本地化支持
若是将ScriptManager的EnableScriptLocalization属性设置为true,那么可以在<asp:ScriptReference>标签中使用ResourceUICultures属性指定一系列的本地化脚本的区域名称。ScriptManager将在运行时检查客户浏览器的区域设置,然后自动将得到的区域名称和<asp:ScriptReference>标签的ScriptPath属性结合起来,最终发送给客户端相应的、经过了本地化的脚本。
EnableScriptLocalization属性接受一个由逗号(,)分隔开的列表,每一项代表一个区域名称,例如ResourceUICultures="zh-CN,en-US"。
我们来看下面的这段代码,注意其中粗体部分:
<asp:ScriptManager ID="ScriptManager1" EnableScriptLocalization="true"
runat="server">
<Scripts>
<asp:ScriptReference Path="~/Scripts/MyControl.js"
ResourceUICultures="zh-CN,en-US" />
</Scripts>
</asp:ScriptManager>
若是用户的区域设置为汉语/中国大陆(即zh-CN),那么ScriptManager将发送下面这段脚本:
<script src="Scripts/MyControl.zh-CN.js" type="text/javascript"></script>
若用户的区域设置为英语/美国(即en-US),则ScriptManager将发送下面这段脚本:
<script src="Scripts/MyControl.en-US.js" type="text/javascript"></script>
注意上面两段最终<script>元素中的src属性(粗体部分),可以看到zh-CN和en-US与/Scripts/MyControl.js组合了起来,页面中实际引用的是Scripts/MyControl.zh-CN.js或Scripts/MyControl.en-US.js这两个脚本。若我们在这两个脚本中给出相应的不同语言版本的资源,那么一个允许本地化的Ajax程序就这样完成了!
若要使用ScriptManager内建的这种本地化功能,我们需要注意以下几点:
(1) 必须设定ScriptManager的EnableScriptLocalization属性为true。
(2) 必须使用ScriptPath直接引入脚本,而不能使用Assembly和Name属性引入内嵌在程序集中的脚本。
(3) 确保ResourceUICultures属性指定的各个区域都有与之相配的脚本文件。例如在上面的例子中,ResourceUICultures属性设置为了zh-CN和en-US,那么我们就要保证网站给出了Scripts/MyControl.zh-CN.js和Scripts/MyControl.en-US.js这两个脚本。
3.3.9 通知脚本资源加载完成
在浏览器成功下载MicrosoftAjax.js之后,客户端ASP.NET AJAX框架即刻开始初始化。因此后续的脚本文件必须在下载完成时发出通知,以便客户端ASP.NET AJAX框架接下来进行这部分脚本的初始化等后续操作。
在ASP.NET AJAX页面中,每一个将要与客户端框架协同运行的脚本文件的末尾都必须包含如下一行代码,用来发出这个通知:
if(typeof(Sys) !== "undefined")Sys.Application.notifyScriptLoaded();
因此,我们在编写自定义的脚本文件时,也要在末尾添加上面这一行。
但如果该脚本文件嵌入到某个程序集中,并通过<asp:ScriptReference>的Assembly和Name属性引入,那么ASP.NET AJAX将自动在文件末尾添加上述这一行通知脚本资源加载完成的声明。若脚本文件本身已经有了这一行,将导致这行代码被执行两次,进而引发异常。
为了解决这个问题,<asp:ScriptReference>特意引入了NotifyScriptLoaded这个布尔值属性。其默认值为true,即自动为嵌入到程序集中的脚本添加通知脚本资源加载完成的声明。若已经为该脚本添加了该声明,那么应该将该属性设置为false,避免重复通知。请参考如下代码,注意其中粗体部分:
<asp:ScriptManager ID="ScriptManager1" runat="server">
<Scripts>
<asp:ScriptReference Assembly="Dflying.Script"
Name="SomeScript.js"
NotifyScriptLoaded="false" />
</Scripts>
</asp:ScriptManager>







