65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET 与 jQuery 的极致运用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (88投票s)

2010年7月21日

CPOL

20分钟阅读

viewsIcon

264965

downloadIcon

3851

一种使用 jQuery 和 ASP.NET 进行 AJAX Web 开发的有机方法

让 Update Panel 去死吧

或者说,我们如何以真正的 AJAX 风格使用 ASP.NET 并快乐地生活

UpdatePanel 代表了微软对 AJAX 技术的主流诠释。UpdatePanel 不可否认的优点是,它为任何 ASP.NET 程序员的日常编程工作带来了 AJAX 的魔力。但现在,在它诞生三年后,UpdatePanel 向我——一个忠实而忠诚的微软程序员——也展示了它所有的局限性。

让我们来看看为什么 UpdatePanel 开始变得臭不可闻。

UpdatePanel 是一个 ASP.NET 容器控件。

当某些事件发生时,它会与服务器通信,而无需进行完整的页面回发(postback)。通信内容包括向服务器发送事件名称、控件名称、参数、页面控件值、Cookie 以及整个视图状态(viewstate)。

服务器返回一套完整的标记(markup),这些标记必须注入到客户端网页中。

这种方法的缺点是:

  • 我们无法控制带宽占用,因为视图状态和返回的标记可能非常庞大。
  • 由于触发器(trigger)的主导方式以及 UpdatePanel 嵌套时的级联效应,在一个页面中以确定性的方式管理多个 UpdatePanel 非常困难。
  • 一些 ASP.NET 控件如果放在 UpdatePanel 内部就无法工作(例如,验证器),我们被迫使用 ASP.NET AJAX Control Toolkit,这套控件集就像一个黑盒子,专为与 MS AJAX 配合使用而设计。使用这个工具包真的很痛苦,因为要让它的组件在一个复杂的页面中工作,就像一个反复试错的过程。很多时候,我们被迫让组件按照它想要的方式工作,而不是我们想要的方式……

那么我该如何在 ASP.NET 中使用 AJAX 呢?

这关乎勇气、想象力以及对未知的不惧。

几年来,AJAX 已经可以通过 JavaScript 使用。JavaScript 能够修改页面 DOM,动态管理 CSS,并使用 XMLHttpRequest 对象。问题一直在于 JavaScript 很难编写,因为不同的浏览器引擎对它的解释不同。

此外,JavaScript 很难调试,而且由于它不是一种静态类型语言,它需要高水平的编程纪律和对源代码的控制。

如今,这些限制中的一部分已经成为不好的回忆,这要归功于 jQuery

jQuery 是一个伟大的 JavaScript 框架,它保证了在各种现代浏览器中的兼容性。并且,jQuery 也是我们对“如何在 ASP.NET 中使用 AJAX”这个问题的回答。

接下来,我将描述我个人对这个问题的理解。

为了找到一种完整而有机的组织 Web 项目的方法,放弃微软的 AJAX,但继续使用 ASP.NET,我受到了网络上找到的建议和技巧以及个人工作经验的启发。

本文是我对现代 Web 开发理解的全面报告。

您可以下载一个简单的示例项目,以更好地理解以下几点。

项目组织

磨利我们的武器

在演示项目中,您可以看到每个 ASPX 页面都包含 HTML 标记、一些 Web 服务方法和用于初始化控件的 onLoad 服务器方法。除此之外,ASPX 页面不能包含其他方法。

每个页面都有一些 JavaScript 文件来完成所需的任务:从服务器通信到页面渲染。

在客户端页面加载时注册事件

演示项目的主页面是 StuffSelection.aspx

StuffSelection.aspx 需要两个 JavaScript 页面:用于管理事件、暴露函数和应用动态 CSS 样式。

我决定将依赖于 ASPX 页面的 js 文件放在与该页面相同的文件夹中。为了保持项目内部的顺序,任何 js 文件都必须与相关的 ASPX 页面同名。

  • StuffSelection.aspx
  • StuffSelection.js
  • StuffSelection_Proxy.js

StuffSelection.js 中,我们首先需要链接到类别选择器(一个 DropDownList 控件)的 onchange 事件,因为我们必须在客户端注册事件以避免使用服务器事件的方式。

由于 ddlCategory 是一个服务器 ID,首先,我们必须在客户端定位该对象。

我们不能使用 <%= ddlCategory.ClientID %> 来解析客户端 ID,因为我们的 JavaScript 代码不在 ASPX 页面中,但我们可以用一个客户端标签包裹该对象,并使用适当的 jQuery 选择器来找到它(在我们的代码中,它是一个简单的 span)。

通过这种方式,我们可以在 StuffSelection document.ready 函数中为 change 事件加载处理程序。

//File: StuffSelection.aspx 

<span id="spanDdlCategory"> 
  Select Stuff category: <asp:DropDownList ID="ddlCategory" runat="server" />
</span>

JavaScript 代码

//File: StuffSelection.js 

$(document).ready(function () {
  var ddlCategory = $('#spanDdlCategory select');
  ddlCategory.change(function () {
  var categoryValue = $(this).val();
  GetStuffList(categoryValue);
  });
});
 

function GetStuffList(categoryValue) {
  if (categoryValue == 0) {
   $('#divStuffList').html('');
  } 
  else {
   // TODO: Get Stuff from server
  } 
}

jQuery 服务器通信

智能通信的轻盈

我们的 DropDownListonChange 客户端事件调用了 GetStuffList 函数。GetStuffList 的任务是调用服务器以获取该类别的项目列表,并且——如果可能的话——不进行完整的页面回发。我通常会创建另一个 JavaScript 文件来调用服务器。

将 JavaScript 逻辑的不同功能区域物理上分开是件好事。如果你使用清晰的命名规则,就不要犹豫这样做。在接下来的章节中,我们将看到如何优化 JavaScript 文件的泛滥问题。

我们的新 JavaScript 文件将是 StuffSelection_Proxy.js

对于这类辅助文件,我通常会用 static 方法模拟一个 static 类。用 JavaScript 很容易实现这种效果。

//File: StuffSelection_Proxy.js

function StuffSelection_Proxy() { }

StuffSelection_Proxy.GetStuffListHttpGet = 
           function (category, successCallback, failureCallback) {
  $.ajax({
   type: "GET",
   contentType: "application/json; charset=utf-8",
   url: "StuffSelection.aspx/GetStuffListServiceHttpGet?category=" + category,
   success: function (data) { successCallback(data); },
   error: function (data) { failureCallback(data); }
  }); 
}

因此,我们在 StuffSelection.js 文件中的调用函数将变为这样:

//File: StuffSelection.js

function GetStuffList(categoryValue) {
  if (categoryValue == 0) {
   $('#divStuffList').html('');
  } 
  else {
   StuffSelection_Proxy.GetStuffListHttpGet(categoryValue, null, null);
  }
}

通过这种方式,我们将开始从一个名为 GetStuffListService 的 Web 方法请求数据。

稍后,我们将看到如何从 Web 方法接收数据,但现在让我们描述一下 GetStuffList 是如何工作的。

这个“static”方法接收类别 DropDownList 的值和两个回调函数作为参数。目前,只有第一个参数对与我们的服务器方法通信有用。为此,我们使用一种称为 JSON 的数据表示方式。

JSON 将数据表示为键值对集合。由于我们的 Web 方法期望一个名为“category”的单一参数,我们用“category”作为键,类别值作为值来构建我们的 JSON 参数。

结果将是类似 {"category":"1"} 的东西,具体取决于类别的选择。

我们使用 JSON.stringify 方法,它保证了从各种组合元素中得到语法正确的结果。

JSON 是一个在几乎所有浏览器中都可用的静态库,除了旧版的 IE(Internet Explorer 从版本 8 开始原生实现 JSON)。为了解决这个问题,我推荐使用 json2 库,它会检查浏览器是否具有原生的 JSON 功能;否则,它会为应用程序提供 JSON 方法支持(http://www.json.org/json2.js)。

$.ajax 是一个 jQuery 方法,允许向服务器发送请求。默认情况下,通信以异步模式发送,这样用户就不会遇到页面冻结的情况,并且可以向服务器发送多个请求而无需等待任何响应。$.ajax 可以在响应成功时通知,在服务器错误时通知失败。

Url 选项标识客户端请求的端点(endpoint)。端点可以是 Web 服务(.asmx)的方法、Web 处理程序(.ashx)或页面内的 Web 方法(.aspx)。

$.ajax 支持 GETPOST 方法,在演示项目中,我两者都包含了,以展示两种不同的语法。在这里我只展示 GET 调用,它在这种情况下更正确。

//File: StuffSelection.aspx 

[WebMethod] 
[ScriptMethod(UseHttpGet = true, 
 ResponseFormat = ResponseFormat.Json, XmlSerializeString = false)] 
public static IList<Stuff> GetStuffListServiceHttpGet(int category)
{ 
  return StuffHelper.GetStuffList(category);
}

让我们分析一下这次通信的结果(一个简单的向我们的 Web 方法的 post 可以通过 Firebug 进行分析;请参见“使用 Firebug 调试”一章)。

完整的 post 请求大小不到 1 KB,因为它只包含我们带有类别参数的简单请求、一个作为响应的 JSON 字符串,以及可能的 Cookie 和消息头。我们绝不会收到比这更多的数据,没有更多的标记,没有更多的视图状态。太棒了!

通过这种“智能”通信,我们失去了什么?

不多,别担心

嗯……我们失去了控件状态。

乍一看似乎很可怕……没有有状态的控件我们怎么工作?

在 ASP.NET 中,控件状态存在于控件本身和视图状态中(对于不可见的控件)。当一个 UpdatePanel 进行部分回发时,我们在服务器端可以访问到每个页面控件及其一致的状态。

这就是为什么即使在部分回发期间,我们也可以访问文本框的内容、组合框的选中项等等。

但 ASP.NET 不是一个聪明的家伙,因为它无法预测我们在服务器端需要哪些控件状态,所以它会 post 所有东西:可见控件的值和整个视图状态,其中包含大量即使对于部分回发也是无用的信息。对我们来说太重了!

相信我,别担心。有状态控件的方法在旧式的 Web 表单架构中是一个极大的便利,但它不是必需的……不再是了。

我们的服务器如是说

现在我们必须收集服务器的回复。

为此,我们需要实现一个成功回调函数,作为参数传递给我们的代理方法。让我们看看我们的 js 文件又发生了什么变化。

//File: StuffSelection.js 

function GetStuffList(categoryValue) {
  if (categoryValue == 0) {
   $('#divStuffList').html('');
  } 
  else {
   StuffSelection_Proxy.GetStuffListHttpGet(categoryValue, 
              successCallback, failureCallback);
  } 
} 

var successCallback = function (data) {
  var response = eval(data.d);
  $('#divStuffList').html(response[0].Name + ' ' + 
           response[0].Description + ' (€ ' + response[0].Price + ')');
 }

var failureCallback = function (data) {
  alert('Request failure');
}

Firebug 再次帮助了我们。在 successCallback 函数的第一行设置一个断点,看看服务器是如何响应请求的。

data”参数包含一个 JSON 格式的字符串,可以被评估并转换为 JavaScript 对象。

我评估 data.d 是因为“d”是一个容器对象,它包装了 ASP.NET 完成的任何 JSON 序列化。这样,我就有了一个变量响应,它包含一个由 Web 方法返回的对象,即请求类别的 List<Stuff>

当然,List<T> 必须序列化为 JavaScript 能够理解的对象。ASP.NET 序列化器将其转换为一个 T 类型的数组,这对于我们的目标来说已经足够了。

客户端模板

……或者我如何能够完全(并最终)控制我的表格

现在我们有了一个对象数组,我们需要在我们的网页上打印它。

之前的方法

$('#divStuffList').html(response[0].Name + ' ' + response[0].Description ... 

自然是显示结果的快速方法,但它不是渲染对象的最佳方式。

我们最好使用模板。

JavaScript 模板可以通过独立的框架或通过 jQuery 插件来实现。

jQuery 插件是这个神奇框架的隐藏宝藏,因为它们为核心框架提供了大量的图形效果、服务、组件等等。想一想你网页上需要的任何东西,肯定有一个 jQuery 插件可以完成这个任务。

最近,我一直在使用一个名为 jBind 的 jQuery 模板插件。这是一个非常好的插件,但似乎已经很长时间没有得到支持了。所以我不会详细描述这个插件,而是介绍 JavaScript 中模板背后的概念(等待微软的客户端模板插件)。

客户端模板与服务器端模板完全相同。你不再拥有 RepeaterGridView 或其他超级复杂的 ASP.NET 控件,只有一个带有一些占位符的 HTML 片段。通常,模板插件通过将键名绑定到同名标签来链接数据。

如果我们的数据源是一个对象数组,模板插件会重复一组 HTML 标签足够多次以渲染所有对象。

我们可以决定将模板放在将要渲染它的同一页面内(使那段代码不可见),或者从 JavaScript 动态创建标签。

我更喜欢第一种方法,因为我可以很容易地通过取消隐藏来查看我的模板以进行设计。

出于同样的原因,我可以用编辑器绘制我的模板,使用样式和类,并立即查看最终的图形渲染效果。

这是一个简单的模板示例

//File: StuffSelection.aspx 

<div style="display: none;">
  <%-- Templates --%> 
  <%-- Template for stuff list --%>
  <div id="divStuffListTemplate">
   <div>
    <table class="tableList">
     <tr> 
      <td>Name</td>
      <td>Code</td>
      <td>Description</td>
      <td>Price</td>
      <td>Is available</td>
     </tr> 
     <!--data-->
     <tr> 
      <td>{Name}</td>
      <td>{Code}</td>
      <td>{Description}</td>
      <td>€{Price}</td>
      <td>{IsAvailable}</td>
     </tr> 
     <!--data-->
    </table>
   </div>
  </div>
</div>

外部的 div 用于隐藏/显示整个块。最有趣的行是那些包含必须链接到数组对象的占位符的行。

从 JavaScript 的角度来看,我们只需要根据其语法激活我们的插件。在我的例子中,代码是这样的:

//File: StuffSelection.js 

var successCallback = function(data){
  var response = eval(data.d);
  //$('#divStuffList').html(response[0].Name + ' ' + 
  //           response[0].Description + ' (€ ' + response[0].Price + ')');
  $('#divStuffList').html(''); 
  var template = $('#divStuffListTemplate').html();
  $(template).bindTo(response, { fill: true, appendTo: '#divStuffList' });
}

由于客户端模板可以直接应用于 HTML 代码,因此添加类、装饰、样式、编号、交替背景色等等都变得超级容易。而且毫无疑问,为 HTML 设计主题比为 GridView 或任何服务器控件设计皮肤更容易、更灵活。

如果我需要向服务器发送一个复杂类型怎么办?

嗯……在 ASP.NET 中也很难

实际上,用一点 JavaScript 并不难,而且它让我们能够愉快地忘记我们的服务器方法是无状态的(如前所述)。

我们有一个注册新用户的页面(SignupUser.aspx),我们需要向我们的 Web 方法传递大量数据:用户名、名字、姓氏、密码等等。

可以实现一个带有多个参数的 Web 方法,但我们的目的是构建一个清晰且可维护的结构。换句话说,我们想向服务器传递一个复杂的对象。

通过 ASP.NET 的序列化过程(以及反序列化),这是自动完成的。

在我们的 JavaScript 中,我们定义了一个名为 UserJs 的简单伪类。

//File: SignupUser_User.js 

function UserJs() { 
  this.FirstName = ''; 
  this.Surname = '';
  this.Username = '';
  this.Password = '';
}

我们可以在服务器上做同样的事情

//File Ecommerce.Business.UserJs 

public class UserJs 
{ 
  public string FirstName { get; set; }
  public string Surname { get; set; }
  public string Username { get; set; }
  public string Password { get; set; }
}

这就是在客户端和服务器之间交换数据所需要做的全部事情。

如果我们的 Web 方法 RegisterUserServiceSignupUser.aspx 页面中期望一个 Ecommerce.Business.UserJs 参数,我们必须从 JavaScript 传递一个序列化的对象,该对象具有相同的名称、相同的字段(相同的字段名)和相同的类型(如果无法自动转换)。就是这样。

//File: SignupUser.js

var user = new UserJs();
user.FirstName = $('#firstName').val();
user.Surname = $('#surname').val();
user.Username = $('#userName').val();
user.Password = $('#password').val();
SignupUser_Proxy.RegisterUser(user, successCallback, failureCallback);

在示例项目中,让我们尝试在 Visual Studio 的调试模式下注册一个用户,以检查 Web 方法中反序列化后的 UserJs 实例。

如果我需要在加载时从服务器接收一个复杂类型呢?

同样的问题,但方向不同

我们不能在页面加载期间依赖于调用特定的 Web 方法来为我们的客户端控件提供初始值。想象一下,你有多个控件需要填充数据,而你不想大量使用 ASP.NET 服务器控件。

有一个简单但非常聪明的变通方法,可以为客户端提供服务器端可用的值。

不要考虑用 RegisterClientScriptBlockRegisterStartupScript 函数从服务器注入 JavaScript 变量。我们正在努力使我们的新编程方式变得干净和优雅。

相反,请看这个网页:http://west-wind.net/Weblog/posts/259442.aspx

这位夏威夷小伙写了一个很棒的类,它允许在服务器上加载一个键值对对象集合,并在客户端以一种哈希表的形式使其可用。

让我们看看我们演示项目中的 GetListFromServerVars.aspx

这个页面通过 BasePage 提供的 AddClientVariables 方法加载整个 Category 集合(类别名称 - 类别值)。

然后,GetListFromServerVars.js 读取所有值并将它们打印在页面上。

//File: GetListFromServerVars.aspx

foreach(Category category in categoryList)
{  
  base.AddClientVariable(category.Key, category.Value);
}

//File: GetListFromServerVars.js 

var html = 'Computer: ' + serverVars["Computer"];
html += '<br/>Monitor: ' + serverVars["Monitor"];
html += '<br/>Keyboard: ' + serverVars["Keyboard"];

最小化 JavaScript

优化的禅意花园

到目前为止,我们还没有关心 JavaScript 文件的泛滥问题。我欣赏一种将源代码分成多个逻辑单元,从而分成多个文件的开发方法。这关乎组织、秩序和可维护性。

但这有一个缺点。如果我们的 Web 服务器必须分发很多小文件而不是一个大文件,我们就会损失带宽和性能。

但有一个简单的解决方法:最小化和合并过程。

场景

我们的项目每个 ASPX 页面都有很多 JavaScript 文件。在 JavaScript 文件夹中,我们有一些 jQuery 插件。

解决方案

在示例项目中,有一个名为“lib”的文件夹,其中包含一个名为 jsmin.exe 的程序(参见 http://www.crockford.com/javascript/jsmin.html)。Jsmin 可以最小化和合并 JavaScript 文件。

现在转到 EcommerceWebSite 项目并查看其属性。在“生成事件”选项卡中,我插入了一个后期生成事件命令。

type "$(ProjectDir)js\jquery.*.js" | "$(ProjectDir)..\..\lib\jsmin" > 
     "$(ProjectDir)js\min\Plugins.min.js" %copyright% 

ECHO Minify and merge aspx js
type "$(ProjectDir)SignupUser*.js" | "$(ProjectDir)..\..\lib\jsmin" > 
     "$(ProjectDir)js\min\SignupUser.min.js" %copyright%
type "$(ProjectDir)StuffSelection*.js" | "$(ProjectDir)..\..\lib\jsmin" > 
     "$(ProjectDir)js\min\StuffSelection.min.js" %copyright%
type "$(ProjectDir)GetListFromServerVars*.js" | "$(ProjectDir)..\..\lib\jsmin" > 
     "$(ProjectDir)js\min\GetListFromServerVars.min.js"

后期生成命令负责将 JavaScript 文件夹中找到的所有 jQuery 插件提供给 jsmin.exe。Jsmin 创建一个单一的 JavaScript 文件,并将其复制到 js/min 文件夹中。

同样,对于任何需要 JavaScript 文件的 ASPX 页面,都必须配置后期生成命令。

每个 ASPX 页面都会生成一个最小化的 JavaScript 文件。例如,对于 StuffSelection.aspx 页面,将生成一个名为 StuffSelection.min.js 的单一 JavaScript 文件。

此时,一个大问题出现了:我们如何使用最小化的 JavaScript 文件,同时保留调试客户端脚本的能力?最小化和合并后的代码即使是优秀的程序员也难以理解。因此,我们需要保持同时拥有最小化和未最小化文件的能力。

解决方案是在各种网站上发现的一个简单而绝妙的想法。从 ASPX 链接的 JavaScript 文件取决于我们配置中的一个参数。

当我们在 web.config 文件中设置调试模式(<compilation debug="true"/>)时,我们链接到原始的 JavaScript 文件。而当我们设置发布模式(<compilation debug="false"/>)时,我们链接到最小化的文件。

因此,我们的 ASPX 文件会像这样改变:

//File: StuffSelection.aspx

<% if (HttpContext.Current.IsDebuggingEnabled) { %>

  <script src="StuffSelection.js" type="text/javascript"></script>

  <script src="StuffSelection_Proxy.js" type="text/javascript"></script>

<% } else { %>

  <script src="js/min/StuffSelection.min.js" type="text/javascript"></script>

<% } %>

通过这种方式,我们可以通过更改 web.config 中的一个简单属性来获得源代码或最小化文件。

当然,如果我们想即使在生产环境中也能调试客户端脚本,我们必须同时发布源 JavaScript 和最小化的 JavaScript。

全部缓存起来

当一个好功能可能引起麻烦时

我在这里介绍的 Web 编程方法会产生各种后果。其中之一是整个 UI 逻辑和一些业务逻辑从服务器端转移到了客户端。换句话说,这意味着你必须编写大量的 JavaScript 代码。

我认为这是一个极好的优点,但也有一些小缺点。一个缺点是我们被迫深入了解各种浏览器,它们成为我们开发技术的重要工具。

jQuery 在隐藏不同浏览器引擎的大多数特性方面帮助很大,但游戏还没有结束。

让我们来看一个简单的例子。

想象一下,在一个安静而无聊的星期五下午,你的老板走进你的办公室,提出了通常的要求:“你能改变一下那个 Web 应用里我按那个按钮时出现的消息吗……”。老板们特别以在星期五下午提出这类要求而闻名。

消息自然是硬编码在一个 JavaScript 文件中。

“好的”——你想——“这次任务很简单,我应该不用待到很晚”。

你更改了本地的 JavaScript 文件,测试了它,提交了它(你有源代码控制系统,对吧?),然后将它复制到测试和生产环境。所有这些都在五分钟内完成,而且没有重新编译整个项目。

现在你可以自豪地去告诉你的老板:“搞定了”。

你的老板非但没有对你超快的执行速度感到惊讶,反而去了网站,按了那个按钮,然后……发现消息没有改变。

“该死的缓存”你心想。

不管怎样,解决方案很简单:按 CTRL + F5,浏览器就会刷新它的缓存,JavaScript 文件被正确地重新加载,一切顺利……但我们不能强迫我们的用户按 CTRL + F5 来确保他们拥有新文件。而且你的老板对此也不是很高兴……

问题在于浏览器在管理缓存方面非常激进。

如果你观察一个页面请求期间发生的事情,你可能会感到惊讶。要监控对服务器的请求,你可以像往常一样使用 Firebug,或者另一个很棒的免费工具,叫做 Fiddler

这两个工具都可以监控每个页面请求,并将其分解为基本部分。

你会看到对于每个请求的资源,浏览器可以:

  • 下载它(状态码 200)
  • 在与服务器核对是否已更改后从缓存中获取(状态码 304)
  • 直接从缓存中获取(资源根本不出现)

我不知道每个浏览器遵循什么规则来选择正确的选项,但我确切地知道,在某些情况下,一些浏览器会在这项任务上严重失败。

可能的解决方案是玩弄请求头,指示浏览器缓存或不缓存某些资源,但这种方法也相当不可预测。

所以我们有两种更简单、更安全的方法来解决这种情况:

  1. 当资源内容改变时,改变资源的名称。

    这是 jQuery 开发者使用的方法(jQuery 1.4.1 是一个与 jQuery 1.4.2 不同的资源,你的浏览器被迫下载它)。但这种方法很难实现,因为当你改变一个资源名称时,你必须改变对该资源的所有引用。这是一项手动任务,容易出错和遗漏,因此我们不太喜欢它。它适用于 JavaScript 组件或框架文件,但不适用于应用程序文件。

  2. 你可以指示你的浏览器用一个 GET 请求来请求资源。

    code.js?version=1 不同于 code.js?version=2,即使浏览器缓存了 code.js 文件,它也肯定会请求新版本。

    通过这种方式,你可以集中管理“version”变量,例如,将它放在应用程序的 web.config 文件中,并在每次修改任何 JavaScript 文件后更改该值。

    在 ASPX 文件中,你可以写一些类似下面的代码:

    <script src="code.js?version=<%= Config.Version %>" type="text/Javascript">
    </script>

    一步更新你所有项目中的所有引用。

当我第一次在网上发现第二种方法时,我非常喜欢它,但它并不完美。当我只在一个 JavaScript 文件中更改一个简单的消息时,我迫使浏览器下载应用程序所有网页中的所有 JavaScript 文件,因为版本值对每个文件都改变了。

为了避免这种浪费,我简单地写了一个我称之为 ResolveAndVersionUrl 的方法,它解析 URL 并向解析后的资源名称添加一个代表文件哈希的值。

通过这种方式,只有实际更改的资源才会改变它们的查询字符串。

嗯……我没疯!我不会让我的应用程序在每个页面请求时都计算每个 JavaScript 资源的哈希值。对于这项任务,ASP.NET 缓存变得非常有用。你可以在 Controls/BasePage.cs 中看到我的算法的简化示例,以及在 CacheJs.aspx 页面中的典型用法。

在这个例子中,缓存会存活一天,并且它依赖于文件系统上的一个文件(当我更改一个 JavaScript 文件时,我必须记得删除依赖的文件,以使应用程序重新计算所有文件的哈希值)。

由于每个 JavaScript 资源都是通过 GET 请求的,浏览器被迫向服务器请求它,并且只有在资源没有改变的情况下,才从其缓存中获取。

一个收到状态 304 的请求会占用一点流量,但由于我们已经将所有页面的 JavaScript 资源合并到一个文件中,所以总的流量权重是可以接受的。

使用 Firebug 调试

……我们最好的朋友

Firebug 是一个很棒的 Firefox 插件。

它最好的功能之一是通过多种工具调试客户端脚本:

  • 断点(绝对和条件断点)
  • 自动和手动监视器
  • 上下文检查器
  • 调用堆栈
  • 求值控制台
  • 用于在调试中前进的标准快捷键

Firebug debug

从版本 8 开始,Internet Explorer 也有了自己的开发者控制台,功能也很强大。但旧习难改,所以我最好的朋友仍然是 Firebug。

Firebug 允许监控对服务器的每一次 post,甚至包括那些源自客户端脚本的 post。

通过这种方式,你可以监控传递给 Web 方法的参数以及作为回复获得的数据。

Firebug Net

我建议与 Firebug 一起安装 Firecookie,以监控、管理和编辑 Cookie。

有了这些工具,你的工具箱对于任何关于客户端调试的任务来说都相当完整了。

JavaScript 中的区域 (Regions)

锦上添花

是的,现在你的 JavaScript 代码在读完这篇文章后开始变得越来越多,你可以用区域来组织它。

你只需要在 Visual Studio 中包含以下宏代码:

Option Explicit On 
Option Strict On 
 
Imports System 
Imports EnvDTE 
Imports EnvDTE80 
Imports EnvDTE90 
Imports System.Diagnostics 
Imports System.Collections.Generic 
Imports System.Text.RegularExpressions 
 
Public Module JsMacros 
 
    Sub OutlineRegions() 
        Dim selection As EnvDTE.TextSelection = _
            CType(DTE.ActiveDocument.Selection, EnvDTE.TextSelection) 
 
        Const REGION_START As String = "//\s*?\#region" 
        Const REGION_END As String = "//\s*?\#endregion" 
 
        selection.SelectAll() 
        Dim text As String = selection.Text 
        selection.StartOfDocument(True) 
 
        Dim startIndex As Integer 
        Dim endIndex As Integer 
        Dim lastIndex As Integer = 0 
        Dim startRegions As New Stack(Of Integer) 
 
        Dim rStart As New Regex(REGION_START, RegexOptions.Compiled) 
        Dim rEnd As New Regex(REGION_END, RegexOptions.Compiled) 
 
        Do 
 
            Dim matchStart As Match = rStart.Match(text, lastIndex) 
            Dim matchEnd As Match = rEnd.Match(text, lastIndex) 
 
            startIndex = matchStart.Index 
            endIndex = matchEnd.Index 
            If startIndex + endIndex = 0 Then 
                Return 
            End If 
 
            If matchStart.Success AndAlso startIndex < endIndex Then 
                startRegions.Push(startIndex) 
                lastIndex = startIndex + 1 
            Else 
                ' Outline region ... 
                Dim tempStartIndex As Integer = CInt(startRegions.Pop()) 
                selection.MoveToLineAndOffset(CalcLineNumber(text, _
                          tempStartIndex), CalcLineOffset(text, tempStartIndex)) 
                selection.MoveToLineAndOffset_
		(CalcLineNumber(text, endIndex) + 1, 1, True) 
                selection.OutlineSection() 
 
                lastIndex = endIndex + 1 
            End If 
        Loop 
 
        selection.StartOfDocument() 
    End Sub 
 
    Private Function CalcLineNumber(ByVal text As String, _
                     ByVal index As Integer) As Integer 
        Dim lineNumber As Integer = 1 
        Dim i As Integer = 0 
 
        While i < index 
            If text.Chars(i) = vbLf Then 
                lineNumber += 1 
                i += 1 
            End If 
 
            If text.Chars(i) = vbCr Then 
                lineNumber += 1 
                i += 1 
                If text.Chars(i) = vbLf Then 
                    i += 1 'Swallow the next vbLf 
                End If 
            End If 
 
            i += 1 
        End While 
 
        Return lineNumber 
    End Function 
 
    Private Function CalcLineOffset(ByVal text As String, ByVal index As Integer) _
		As Integer 
        Dim offset As Integer = 1 
        Dim i As Integer = index - 1 
 
        'Count backwards from //#region to the previous line counting the white spaces 
        Dim whiteSpaces = 1 
        While i >= 0 
            Dim chr As Char = text.Chars(i) 
            If chr = vbCr Or chr = vbLf Then 
                whiteSpaces = offset 
                Exit While 
            End If 
            i -= 1 
            offset += 1 
        End While 
 
        'Count forwards from //#region to the end of the region line 
        i = index 
        offset = 0 
        Do 
            Dim chr As Char = text.Chars(i) 
            If chr = vbCr Or chr = vbLf Then 
                Return whiteSpaces + offset 
            End If 
            offset += 1 
            i += 1 
        Loop 
 
        Return whiteSpaces 
    End Function 
 
End Module

现在将你的宏代码与一个自定义快捷键链接起来,瞧:每当你按下快捷键时,区域就会被激活。

第二部分

你可以在这里找到本文的第二部分。

历史

  • 2011年1月10日:初始版本
© . All rights reserved.