[IT168技术文档]
原文地址:http://www.codeproject.com/Ajax/aspnetajaxtips.asp
源码下载:http://www.codeproject.com/Ajax/aspnetajaxtips/ASPNETAjaxTips.zip
原文发布日期:2006.12.22
介绍
微软最近发布了ASP.NET AJAX的Beta 2版。虽然它是一个非常强大的框架,但是当你在web 2.0的世界中要开发一个真正的AJAX web站点的话,就会遇到很多问题,而且你几乎找不到任何相关文档。本文中,我将介绍一些在开发Pageflakes中所学习到的高级经验。我们将会看到ASP.NET AJAX一些功能的优缺点,如批调用(Batch Call),调用超时,浏览器调用拥堵问题,ASP.NET 2.0中web service响应缓存的bug等等。
本文最后更新:
为什么要使用ASP.NET
一些人在看到Pageflakes的时候,首先产生的问题就是“为什么不使用Protopage或者Dojo库?而是Atlas?”微软的Atlas(已重命名为ASP.NET AJAX)是一个非常有前途的AJAX框架。微软为了这个框架做了很多努力,制作了大量可重用的组件,这样可以减少项目的开发时间,这样的话只要很少的工作量即可让用户的web应用程序有一个完美的界面。它与ASP.NET融为一体,并且兼容ASP.NET的Membership和Profile。AJAX Control Toolkit项目包括了28个扩展(译注:现在有34个),可以简单的把它们拖拽到页面上,然后通过一些属性设置就可以产生非常酷的效果。看看那些例子你就会知道ASP.NET AJAX带来了多么强大的功能。
在我们最初开发Pageflakes的时候,Atlas还处于幼儿阶段。我们只能使用page method和Web Service方法来调用Atlas的特性。开发者们不得不开发自己的拖拽、组件构造、弹出、伸缩/展开等特性。但是现在,Atlas提供了所有这些功能,所以可以大大节省我们的开发时间。最令人惊奇的是Atlas提供了web service代理的特性。你可以指定<script>标签到一个.asmx文件,并得到一个JavaScript类,它会根据web service的定义被正确的生成。这使得添加或移除一个web service变得非常简单,而且在web service中添加或移除方法都不需要客户端作任何改变。Atlas也提供了很多基于AJAX调用的控件,并且提供了在JavaScript中捕获丰富异常信息的特性。服务器端的异常可以被精确的抛给客户端的JavaScript代码,你可以捕获它们并格式化这些错误信息,然后显示给用户。Atlas与ASP.NET 2.0结合起来工作得非常出色,完全消除了整合问题。完全不需要担心page method和web service的验证和授权问题,所以可以大大减少客户端代码的开发量(当然,也正是因为如此,Atlas运行时也变得非常巨大),相对于其它的AJAX框架来说,将有机会把更多的精力放到自己的代码开发上来。
Atlas最近的版本可以与ASP.NET的Membership、Profile完美的结合,提供了在JavaScript中登录和注销的特性,而不用发postback给服务器,将可直接从JavaScript中读取和写入Profile。当需要在web应用程序中使用Membership和Profile的时候,这将变得非常容易,例如我们做的Pageflakes
在Atlas的早些版本中,没有使用HTTP GET调用的方法。所有的调用都是HTTP POST,所以调用的代价是非常大的。而现在可以方便调用是HTTP GET,一旦使用HTTP GET就可以利用HTTP响应缓存特性,我将很快介绍这一特性。
批调用并不一定快
ASP.NET AJAX的CTP版本(之前的版本)里有一个特性,就是允许在一个请求里包含多个请求。它工作时你不会注意到任何事情,而且也不需要写任何特殊的代码。一旦使用了批调用特性,那么在一次批调用期间,其中所有的web service调用都会被执行,所以它将减少回发时间和总响应时间。
实际的响应时间可能减少了,但是感觉延迟却变长了。如果有3个web service被批量调用,那么第一个调用不会最先完成,而是所有3个调用会在相同的时间完成。如果每一个web service调用完成后都更新UI的话,将不会一步一步更新,而是在所有调用一起完成后再一起更新UI。结果不会看到UI被快速更新,而是在UI被更新之前有一个长时间的延迟。如果说调用中的任何一个(比如第3个调用)下载了大量数据,那么在所有的3个调用完成之前用户不会看到任何变化。所以第一个调用的执行时间几乎接近3个调用的总执行时间。虽然减少了实际的总计处理时间,但是会感觉有更长的延迟。当每个调用都只传输少量数据的时候批调用是非常好的,这样,3个小型调用就会在一次回发中执行完。
让我们看看3个调用是如何一个一个被完成的,这将说明这些调用实际上是如何被执行的。

第二个调用到达服务端的时间要比第一个调用长,因为第一个调用吃光了带宽。同样的原因,下载也就会花更多的时间。浏览器同时打开了两个连接到服务器端的连接,所以在同一时间,只能处理两个调用。一旦第一个调用或第二个调用完成后,第三个调用才能被处理。
当3个web service在一次请求中被批调用的时候:

这里总计下载时间将会有所减少(如果IIS的压缩功能启用的话),并且只需一次网络响应。所有的3个调用一次被发往到服务端后全部执行,组合而成的三个调用的响应是在一次调用中下载。但是对于用户来讲,他们会感觉速度变慢了,因为UI的更新发生在所有批调用完成之后。这个批调用完成的总时间总是要长于两个调用的。并且,如果有大量的一个又一个的UI更新,IE将会冻结一段时间,这将给用户带来一个糟糕的体验。一些时候,时间较长的UI更新会导致浏览器出现“白屏”,但是FireFox和Opera不会有此问题。
批调用也是有一些优点的。如果你的IIS启用了gzip压缩功能的话,将对全部结果进行压缩而不是分别压缩每个结果,那么总下载时长就会少于单独调用的下载时长。所以,通常批调用都是一些小型调用的话会比较好。但是如果调用会发送或者返回较大数据的话,比如20KB,那么最好就别使用批调用了。批调用还有另一个问题,比如说前两个调用非常小,第3个调用十分大,如果这3个调用被批调用的话,那么前两个小的调用就要延迟很长时间,因为第3个很大。
糟糕的调用会使好的调用超时
如果有两个HTTP调用不知何故执行了很长的时间,那么这两个糟糕的调用也将会使好的调用超时,同时这两个调用会进入队列。这里就有一个例子:
Collapse
![]()
function TestTimeout()
![]()
{
![]()
debug.trace("--Start--");
![]()
TestService.set_defaultFailedCallback(
![]()
function(result, userContext, methodName)
![]()
{
![]()
var timedOut = result.get_timedOut();
![]()
if( timedOut )
![]()
debug.trace( "Timedout: " + methodName );
![]()
else
![]()
debug.trace( "Error: " + methodName );
![]()
});
![]()
TestService.set_defaultSucceededCallback( function(result)
![]()
{
![]()
debug.trace( result );
![]()
});
![]()
![]()
![]()
TestService.set_timeout(5000);
![]()
TestService.HelloWorld("Call 1");
![]()
TestService.Timeout("Call 2");
![]()
TestService.Timeout("Call 3");
![]()
TestService.HelloWorld("Call 4");
![]()
TestService.HelloWorld("Call 5");
![]()
TestService.HelloWorld(null); // 这句将导致错误
![]()
}
![]()
服务端的web service也非常简单:
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ScriptService]
public class TestService : System.Web.Services.WebService ...{
![]()
public TestService () ...{
![]()
// 如果使用设计的组件,请取消注释以下行
// InitializeComponent();
}
![]()
[WebMethod][ScriptMethod(UseHttpGet=true)]
public string HelloWorld(string param) ...{
Thread.Sleep(1000);
return param;
}
![]()
[WebMethod][ScriptMethod(UseHttpGet=true)]
public string Timeout(string param) ...{
Thread.Sleep(10000);
return param;
}
}
我调用了服务端的名为“Timeout”的方法,它不会做任何事情,而只是等待一个较长的时间以使调用超时。之后再调用一个不会超时的方法。但是你猜猜输出的是什么:

只有第一个调用成功了。所以,任何时候如果浏览器的两个连接都处在拥堵状态的话,那么你期待的其他调用也都将会超时。
在Pageflakes的运营中,我们曾经几乎每天都从客户端得到400个到600个超时错误报告,却从未能发现这是怎么发生的。起初怀疑是互联网连接过慢造成的,但是不可能如此多的用户都发生这种情况。后来,我们猜测是主机提供商的网络出现了问题。经过了大量的网络析去寻找问题是否是出现在网络上,但是依旧没有没有发现任何异常。接着使用SQL Profiler去查找是否是长时间运行的查询导致了ASP.NET请求执行时的超时。但是不幸,我们最终发现的是,大部分超时错误出现的情况都是先有一些坏的调用,然后好的调用也超时了。所以我们修改了Atlas运行时,引进了自动重试的功能,问题终于完全消失了。然而,自动重试需要对ASP.NET AJAX框架的Javascript做一次“心脏外科手术”,这个方法会要求每个调用在超时后都重试一次。为了实现它,我们需要截获所有web method调用并且在onFailure回调函数中作个钩子,如果失败的原因是超时,onFailure将再次调用相同的web method
另一个需要关注的发现是当我们外出旅行感到疲惫时,想通过酒店或机场的无线网络连接到互联网访问Pageflakes的时候,首次访问总是不成功,并且所有的web service调用在第一次尝试中总是失败。直到我们刷新之前都不会工作。这也是我们要实现web service调用立即自动重试的另一个主要原因,它正好可以解决这个问题。
这里我会告诉你怎么做。Sys$Net$WebServiceProxy$invoke函数是负责处理所有web service调用的。所以需要通过一个自定义onFailure回调函数来替换这个函数。只要有错误或者超时就会激发这个自定义回调函数。当有超时发生的时候,就会再次调用这个函数,重试就会发生。
Collapse
![]()
Sys.Net.WebServiceProxy.retryOnFailure =
![]()
function(result, userContext, methodName, retryParams, onFailure)
![]()
...{
![]()
if( result.get_timedOut() )
![]()
...{
![]()
if( typeof retryParams != "undefined" )
![]()
...{
![]()
debug.trace("Retry: " + methodName);
![]()
Sys.Net.WebServiceProxy.original_invoke.apply(this, retryParams );
![]()
}
![]()
else
![]()
...{
![]()
if( onFailure ) onFailure(result, userContext, methodName);
![]()
}
![]()
}
![]()
else
![]()
...{
![]()
if( onFailure ) onFailure(result, userContext, methodName);
![]()
}
![]()
}
![]()
![]()
![]()
Sys.Net.WebServiceProxy.original_invoke = Sys.Net.WebServiceProxy.invoke;
![]()
Sys.Net.WebServiceProxy.invoke =
![]()
function Sys$Net$WebServiceProxy$invoke(servicePath, methodName, useGet,
![]()
params, onSuccess, onFailure, userContext, timeout)
![]()
...{
![]()
var retryParams = [ servicePath, methodName, useGet, params,
![]()
onSuccess, onFailure, userContext, timeout ];
![]()
![]()
![]()
// 初始调用失败
![]()
// 处理自动重试
![]()
var newOnFailure = Function.createDelegate( this,
![]()
function(result, userContext, methodName)
![]()
...{
![]()
Sys.Net.WebServiceProxy.retryOnFailure(result, userContext,
![]()
methodName, retryParams, onFailure);
![]()
} );
![]()
![]()
![]()
Sys.Net.WebServiceProxy.original_invoke(servicePath, methodName, useGet,
![]()
params, onSuccess, newOnFailure, userContext, timeout);
![]()
}
![]()
运行的时候,它将把每个超时调用都重试一次

这里可以看到第一个方法成功了,所有其他超时的调用都会被重试。而且重试一次后的调用都成功了。发生这种情况是因为在重试中服务端的方法不会做超时处理。这证明了我们的实现方法是正确的。
当队列里有多于两个调用的时候浏览器将不会响应
可以尝试这样做,在首次访问时打开任何一个加载了大量RSS的页(如Pageflakes, Netvibes, Protopage),在加载期间,你可以尝试着单击一个链接到另一个站点或者试着直接访问另一个站点,那么将会发现浏览器不会有任何响应。直到浏览器里所有队列的AJAX调用都完成之后,浏览器才能接受另一个活动。这是IE的一个比较糟糕的地方,同样Firefox和Opera就不会有此问题。
这个问题是当有大量的AJAX调用的时候,浏览器会将所有的调用放到一个队列里,在同一时间内只执行其中的两个。所以,如果你单击了某个链接或者转向另一个站点,那么浏览器必须等待在得到另一个调用之前正在执行的调用完成之后才会去处理。解决这个问题的办法就是防止浏览器在同一时间内有多于两个的调用在队列里。我们需要维持一个自己的队列,然后从这个队列里将一个一个调用的发到浏览器的队列中。
这个解决方案是很棒,它可以防止调用间的冲突:
Collapse
![]()
var GlobalCallQueue = ...{
![]()
_callQueue : [], // 保存web method的调用列表
![]()
_callInProgress : 0, // 浏览器目前处理的web method的编号
![]()
_maxConcurrentCall : 2, // 同一时间内执行调用的最大数
![]()
_delayBetweenCalls : 50, // 调用执行之间的延迟
![]()
call : function(servicePath, methodName, useGet,
![]()
params, onSuccess, onFailure, userContext, timeout)
![]()
...{
![]()
var queuedCall = new QueuedCall(servicePath, methodName, useGet,
![]()
params, onSuccess, onFailure, userContext, timeout);
![]()
![]()
![]()
Array.add(GlobalCallQueue._callQueue,queuedCall);
![]()
GlobalCallQueue.run();
![]()
},
![]()
run : function()
![]()
...{
![]()
/**//// 从队列里执行一个调用
![]()
![]()
![]()
if( 0 == GlobalCallQueue._callQueue.length ) return;
![]()
if( GlobalCallQueue._callInProgress <
![]()
GlobalCallQueue._maxConcurrentCall )
![]()
...{
![]()
GlobalCallQueue._callInProgress ++;
![]()
// 得到第一个调用队列
![]()
var queuedCall = GlobalCallQueue._callQueue[0];
![]()
Array.removeAt( GlobalCallQueue._callQueue, 0 );
![]()
![]()
![]()
// 调用web method
![]()
queuedCall.execute();
![]()
}
![]()
else
![]()
...{
![]()
// 达到最大并发数,不能运行另一个调用
![]()
// 处理中的webservice method
![]()
}
![]()
},
![]()
callComplete : function()
![]()
...{
![]()
GlobalCallQueue._callInProgress --;
![]()
GlobalCallQueue.run();
![]()
}
![]()
};
![]()
![]()
![]()
QueuedCall = function( servicePath, methodName, useGet, params,
![]()
onSuccess, onFailure, userContext, timeout )
![]()
...{
![]()
this._servicePath = servicePath;
![]()
this._methodName = methodName;
![]()
this._useGet = useGet;
![]()
this._params = params;
![]()
![]()
![]()
this._onSuccess = onSuccess;
![]()
this._onFailure = onFailure;
![]()
this._userContext = userContext;
![]()
this._timeout = timeout;
![]()
}
![]()
![]()
![]()
QueuedCall.prototype =
![]()
...{
![]()
execute : function()
![]()
...{
![]()
Sys.Net.WebServiceProxy.original_invoke(
![]()
this._servicePath, this._methodName, this._useGet, this._params,
![]()
Function.createDelegate(this, this.onSuccess), // 调用处理完成
![]()
Function.createDelegate(this, this.onFailure), // 调用处理完成
![]()
this._userContext, this._timeout );
![]()
},
![]()
onSuccess : function(result, userContext, methodName)
![]()
...{
![]()
this._onSuccess(result, userContext, methodName);
![]()
GlobalCallQueue.callComplete();
![]()
},
![]()
onFailure : function(result, userContext, methodName)
![]()
...{
![]()
this._onFailure(result, userContext, methodName);
![]()
GlobalCallQueue.callComplete();
![]()
}
![]()
};
![]()
QueueCall封装了一个web method调用,它拥有真实web服务调用的所有参数,并且重写了onSuccess和onFailure回调函数。我们想知道当一个调用完成或者失败了的时候,如何从队列里调出另一个调用。GlobalCallQueue保存了web服务调用的列表。无论何时,当一个web method被调用时,我们先要对GlobalCallQueue中的调用进行排队,并从我们自己的队列里一个一个的执行调用。这样就可以保证浏览器在相同的时间里不会有多于两个的调用,所以浏览器就不会停止响应。
为了确保队列是基于调用的,我们需要像之前那样再次重写ASP.NET AJAX的web method
Sys.Net.WebServiceProxy.original_invoke = Sys.Net.WebServiceProxy.invoke;
![]()
Sys.Net.WebServiceProxy.invoke =
![]()
function Sys$Net$WebServiceProxy$invoke(servicePath, methodName,
![]()
useGet, params, onSuccess, onFailure, userContext, timeout)
![]()
...{
![]()
GlobalCallQueue.call(servicePath, methodName, useGet, params,
![]()
onSuccess, onFailure, userContext, timeout);
![]()
}
![]()