在浏览器中缓存web服务响应可以显著节省带宽
浏览器可以在用户的硬盘里缓存图片、JavaScript、CSS文件,如果XML HTTP调用是一个HTTP GET的话也是可以缓存的。这个缓存是基于URL的。如果是相同URL,且保存在同一个电脑里,那么数据将从缓存里加载,而不会向服务器再次请求。基本上,浏览器可以缓存任何HTTP GET请求并且返回基于URL的被缓存数据。如果你把一个XML HTTP调用作为HTTP Get方式的话,那么服务端将返回一些特殊的头信息,用于通知浏览器对相应做缓存,之后再次调用相同的内容,结果就会立即从缓存中被返回,从而减少了网络传输延迟和下载时间。

在Pageflakes中,我们对用户的状态做了缓存,所以当用户再次访问的时候会从浏览器的缓存里立即得到缓存数据,而不用通过服务端。因此第二次加载时间会变得非常快。我们也缓存了用户的某些行为所产生的结果。当用户再次做相同行为时,缓存结果就会立即从用户的本地缓存中加载,从而减少了网络传输时间。用户会体验到一个快速加载和高响应的站点,获得速度会有明显增长。

这个方法就是处理Atlas web service调用时要使用HTTP GET方式,并且要返回一些明确的HTTP头信息告知浏览器具体要缓存多长时间。如果在响应期间你返回了一个“Expires”头信息,那么浏览器就会缓存这个XML HTTP结果。这里你需要返回两个头信息去通知浏览器缓存结果。
HTTP/1.1 200 OK    
Expires: Fri, 1 Jan 2030    
Cache-Control: public
该信息将通知浏览器要缓存结果直到2030年1月1日。在你处理具有相同参数的同一个XML HTTP调用的时候,就将从电脑的缓存中加载数据,而不会通过服务端。这里还有更多的控制缓存的高级方法。例如,有一个头信息通知浏览器缓存60秒,那么浏览器要在60秒之后才能接触到服务端并获取新的结果。当60秒后浏览器本地缓存过期的时候,它也会防止从代理服务器端获得已缓存的响应。
HTTP/1.1 200 OK    
Cache-Control: private, must-revalidate, proxy-revalidate, max-age=60
 
让我们来尝试着在一个ASP.NET web service方法中产生这样的头信息:
上接[翻译]ASP.NET AJAX之内部揭秘_翻译[WebMethod][ScriptMethod(UseHttpGet=true)]
上接[翻译]ASP.NET AJAX之内部揭秘_翻译public string CachedGet()
上接[翻译]ASP.NET AJAX之内部揭秘_翻译{
上接[翻译]ASP.NET AJAX之内部揭秘_翻译        TimeSpan cacheDuration = TimeSpan.FromMinutes(1);
上接[翻译]ASP.NET AJAX之内部揭秘_翻译        Context.Response.Cache.SetCacheability(HttpCacheability.Public);
上接[翻译]ASP.NET AJAX之内部揭秘_翻译        Context.Response.Cache.SetExpires(DateTime.Now.Add(cacheDuration));
上接[翻译]ASP.NET AJAX之内部揭秘_翻译        Context.Response.Cache.SetMaxAge(cacheDuration);
上接[翻译]ASP.NET AJAX之内部揭秘_翻译        Context.Response.Cache.AppendCacheExtension(
上接[翻译]ASP.NET AJAX之内部揭秘_翻译                     "must-revalidate, proxy-revalidate");
上接[翻译]ASP.NET AJAX之内部揭秘_翻译
上接[翻译]ASP.NET AJAX之内部揭秘_翻译        return DateTime.Now.ToString();
上接[翻译]ASP.NET AJAX之内部揭秘_翻译}
结果就是下列头信息:
上接[翻译]ASP.NET AJAX之内部揭秘_翻译_13
“Expires”头信息被正确的设置。但是问题发生在“Cache-Control”,它显示了“max-age”的值被设置成零,这将阻止浏览器从任何缓存中读取数据。如果你真的想禁用缓存,当然要发送这样一条Cache-Control头信息。结果像是发生了相反的事情。

输出的结果依然是错的,并没有被缓存上接[翻译]ASP.NET AJAX之内部揭秘_AJAX_14
不能改变“max-age”头信息是ASP.NET 2.0中的bug。因为“max-age”被设置成零,而“max-age”的值等于零就意味着不需要缓存,所以ASP.NET 2.0才把“Cache-Control”设置为“private”。所以使ASP.NET 2.0返回正确的缓存响应的头信息是不可行的。

简短节说。反编译HttpCachePolicy类(Context.Response.Cache对象的类)之后,我发现了如下代码:上接[翻译]ASP.NET AJAX之内部揭秘_翻译_15
不知何故,this._maxAge的值会被设置成零,看一下这段代码“if (!this._isMaxAgeSet || (delta < this._maxAge))”,它用于防止_maxAge被设置得过大。由于这个问题,我们需绕过SetMaxAge函数,然后使用反射去直接的设置_maxAge的值。
上接[翻译]ASP.NET AJAX之内部揭秘_翻译[WebMethod][ScriptMethod(UseHttpGet=true)]
上接[翻译]ASP.NET AJAX之内部揭秘_翻译public string CachedGet2()
上接[翻译]ASP.NET AJAX之内部揭秘_翻译{
上接[翻译]ASP.NET AJAX之内部揭秘_翻译        TimeSpan cacheDuration = TimeSpan.FromMinutes(1);
上接[翻译]ASP.NET AJAX之内部揭秘_翻译
上接[翻译]ASP.NET AJAX之内部揭秘_翻译        FieldInfo maxAge = Context.Response.Cache.GetType().GetField("_maxAge",    
上接[翻译]ASP.NET AJAX之内部揭秘_翻译                BindingFlags.Instance|BindingFlags.NonPublic);
上接[翻译]ASP.NET AJAX之内部揭秘_翻译        maxAge.SetValue(Context.Response.Cache, cacheDuration);
上接[翻译]ASP.NET AJAX之内部揭秘_翻译
上接[翻译]ASP.NET AJAX之内部揭秘_翻译        Context.Response.Cache.SetCacheability(HttpCacheability.Public);
上接[翻译]ASP.NET AJAX之内部揭秘_翻译        Context.Response.Cache.SetExpires(DateTime.Now.Add(cacheDuration));
上接[翻译]ASP.NET AJAX之内部揭秘_翻译        Context.Response.Cache.AppendCacheExtension(
上接[翻译]ASP.NET AJAX之内部揭秘_翻译                        "must-revalidate, proxy-revalidate");
上接[翻译]ASP.NET AJAX之内部揭秘_翻译
上接[翻译]ASP.NET AJAX之内部揭秘_翻译        return DateTime.Now.ToString();
上接[翻译]ASP.NET AJAX之内部揭秘_翻译}
 
它将返回下列头信息:上接[翻译]ASP.NET AJAX之内部揭秘_ASP_32
现在“max-age”被设置成了60,所以浏览器将把数据缓存60秒。如果60秒内你使用了相同的调用,那么它将返回相同的结果。下面是一个显示从服务端返回日期的输出结果:上接[翻译]ASP.NET AJAX之内部揭秘_揭秘_33
1分钟后,缓存过期并且浏览器再次向服务器发送一个请求。客户端代码如下:
function testCache()
{
        TestService.CachedGet(function(result)
        {
                debug.trace(result);
        });
}
 
这里还有另一个问题需要解决。在web.config文件中,你会看到ASP.NET AJAX增加了下面这个元素:
<system.web>
                <trust level="Medium"/>
 
它会防止我们设置响应对象的_maxAge,因为它需要反射。所以你必须删除trust元素或者设置它的level属性为Full
<system.web>    
        <trust level="Full"/>
 
当“this”不是你认为的“this”的时候
Atlas回调函数不会在它们被调用的相同上下文中执行 。例如,如果你在一个JavaScript类里像这样使用一个web method
function SampleClass()
{
        this.id = 1;
        this.call = function()
        {
                TestService.DoSomething( "Hi", function(result)
                {
                        debug.dump( this.id );
                } );
        }
}
 
当你调用“call”方法的时候会发生什么?你会在debug中得到“1”吗?不会,你将在debug中得到“null”,因为这个“this”不再是类的实例。这是每一个人经常会犯的错误。这在Atlas的文档里仍然没有相关说明,我发现好多开发人员都花费时间去查找这是什么错误。

原因是这样的。我们知道只要JavaScript事件被触发,那么“this”就是指导致事件发生的那个HTML元素,所以如果你像下面这样写的话:
function SampleClass()
{
        this.id = 1;
        this.call = function()
        {
                TestService.DoSomething( "Hi", function(result)
                {
                        debug.dump( this.id );
                } );
        }
}

<input type="button" id="ButtonID" onclick="o.onclick" />
 
如果你单击了这个按钮,就会发现“ButtonID”代替了“1”。原因就是这个按钮正在调用“call”方法。所以,这个调用在按钮对象的上下文中完成,而“this”就被映射为这个按钮对象。

同样的,当XML HTTP触发了可以捕获和激发回调函数的onreadystatechanged事件的时候,代码执行仍然在XML HTTP的上下文中。它是触发了这个事件的XML HTTP对象。结果,“this”就指向了XML HTTP对象,而不是你自己的在回调函数被声明处的类。

为了使回调函数在类的实例的上下文中激发,所以要让“this”指向类的实例,你需要做如下改变。
function SampleClass()
{
        this.id = 1;
        this.call = function()
        {
                TestService.DoSomething( "Hi",    
                        Function.createDelegate( this, function(result)
                {
                        debug.dump( this.id );
                } ) );
        }
}
 
这里的Function.createDelegate用来创建一个调用“this”上下文下的特定函数的委托。它可以给函数提供“this”的上下文。Function.createDelegate被定义在Atlas运行时。
Function.createDelegate = function(instance, method) {
        return function() {
                return method.apply(instance, arguments);
        }
}
HTTP POST要比HTTP GET慢,但是ASP.NET AJAX默认用的是HTTP POST
默认情况下,ASP.NET AJAX的所有web service调用都使用HTTP POST方式。HTTP POST方式要比HTTP GET方式付出更多的代价,它通过网络传输更多的字节,因此就要占用宝贵的网络传输时间,也使得ASP.NET要在服务端做一些额外处理。所以,在可能的情况下你应该使用HTTP GET方式。但是,HTTP GET方式不允许你将对象作为参数传输,你只能传输数字、字符串和日期。当你处理一个HTTP GET调用的时候,Atlas会构造一个被编码的URL并使用它。所以,你不应该传输很多内容而使URL超过2048个字符。据我目前所知,这是任何URL的最大长度。

为了在一个web service方法中使用HTTP GET方式,你需要用[ScriptMethod(UseHttpGet=true)]属性修饰这个方法:
上接[翻译]ASP.NET AJAX之内部揭秘_翻译[WebMethod] [ScriptMethod(UseHttpGet=true)]    
上接[翻译]ASP.NET AJAX之内部揭秘_翻译public string HelloWorld()
上接[翻译]ASP.NET AJAX之内部揭秘_翻译{
上接[翻译]ASP.NET AJAX之内部揭秘_翻译}
 
POST与GET的另一个问题是,POST需要两次网络传输。当你使用POST的时候,web服务器会先发送一个“HTTP 100 Continue”,这意味着web服务器已经准备好了接收内容。之后,浏览器才会发送实际数据。所以,因为POST请求的初始阶段要比GET方式花费更多的时间,在AJAX程序里网络延迟(你的电脑和服务器之间的数据传输)是要给与足够重视的,因为AJAX适合处理一些小的需要在毫秒级的时间内完成的调用。否则程序会不流畅并且让用户觉得厌烦。

Ethereal是一个很好的工具,它可以侦测到POST和GET的情况下到底发生了什么:
上接[翻译]ASP.NET AJAX之内部揭秘_ASP_38
从上面的图中,你可以看到POST方式在准备发送实际数据之前,要从web服务器请求一段“HTTP 100 Continue”的确认信息,这之后才会传输数据。另一方面,GET方式传输数据是不需要任何确认的。

所以,当你要从服务端下载页的某一部分、一个表格或者是一段文本之类的时候就应该使用HTTP GET方式。而如果要像web form那样以提交的方式发送数据到服务端的话就不应该使用HTTP GET方式。


结论
上面所说的这些高级技巧都已经在Pageflakes中实现了,这里我并没有提及它的详细实现方法,但是原理都提到了,所以,你可以放心地使用这些技术。这些技术将节省你解决问题的时间,也许在开发环境中你从来没认识到这些问题,但是当你大规模部署网站之后,来自世界各地的访问者就将面对这些问题。一开始就正确的实现这些技巧将会大大节省你的开发和客户支持的时间。请同时关注我的博客以获得更多的技巧。