挣脱浏览器的束缚

来源:互联网 发布:2017小说推荐 知乎 编辑:程序博客网 时间:2024/04/28 01:23

作者:老赵点滴

一、前言

(原创作品网址:http://www.cnblogs.com/JeffreyZhao/archive/2007/01/18/Break_the_Browsers_Restrictions_1.html)

  最近在为某个人门户站点作优化。

  从传统意义上来说,这个站点的各方面都属中规中矩。不过作为一个以客户端为中心的Web应用,其性能,尤其是它的感知性能(Perceived Performance),经常会严重受制于浏览器本身。一个没有对客户端数据访问模型经过精心设计和优化的应用,其导致的结果往往就是无法充分利用带宽,让用户等待的时间变长。换句话说,其Perceived Performance需要进一步的提高。

  突破浏览器限制,充分利用带宽,提高性能,尤其是Perceived Performance等等,就是我这次优化的目的。在接下来的几篇文章里,我将以数据说话,探讨浏览器的限制,并从多个方面来谈一下这次优化的各种方式。由于该个人门户使用了ASP.NET AJAX进行开发,因此我也将会给出一些基于ASP.NET AJAX的解决方案,希望会有一定参考价值,对朋友们能有所帮助。

工具

  本着实事求是的原则,我们需要使用数据来说话,于是我们也就需要一些好用的工具。它们可以帮助我们统计各种数据,以便我们进行分析和优化。

  在IE中,我们需要使用Http Watch这个工具来统计页面中每个请求的信息,例如开始时间,持续长度等等,能够轻松得出详细的数据(图1),非常好用。而且对于我们来说,一个Free Edition已经足够使用了。Free Edition虽然无法得到每次请求的所有信息,但是我们已经有了再熟悉不过的Fiddler。我们完全可以通过那些数据使用Excel作出统计图表(图2),进行分析。

图1:访问http://www.google.com的统计数据

图1:访问http://www.google.com的统计数据

图2:使用Office 2007绘制的统计图表

图2:使用Office 2007绘制的统计图表

  在FireFox下面,我一开始使用的是Google Page Load Analyzer,但是发现使用起来实在不方便,它既无法向Http Watch一样得到详细的信息,以便我们作出统计图表;而它自动生成的示意图又非常难看,很难进行分析。后经人提醒,最新的FireBug也有类似的功能。装上一看,果然好用。虽然无法获得精确数据,但是它生成的示意图(图3)已经可以直接进行分析了。

 图3:访问http://www.cnblogs.com时FireBug绘制的统计示意图

图3:访问http://www.cnblogs.com时FireBug绘制的统计示意图

  此外,为了在本地或局域网内模拟低网速的情况,我再推荐一款工具NetLimiter 2 Pro。它能够对于某个程序、进程甚至某个连接在访问网络时的带宽进行限制,无论是因特网、局域网还是本机(图4)。最后,例如IE Dev Toolbar等工具自然也是必备的,我们可以在需要的时候使用它们。

 图4:使用NetLimiter 2 Pro限制IE的带宽

图4:使用NetLimiter 2 Pro限制IE的带宽 

  有了上面这些工具,就可以开始我们的分析优化之旅了。

二、别让脚本引入坏了事

(原创作品网址:http://www.cnblogs.com/JeffreyZhao/archive/2007/01/20/Break_the_Browsers_Restrictions_2.html)

  现在哪里还找得到不引入JavaScript脚本文件的Web应用?使用脚本文件的好处多多,其中最重要的可能就是提供缓存能力了。使用脚本文件之后再加上缓存,可以大大降低数据传输量,提高页面打开的速度。不过脚本文件的引入也不是简单得不值一提,我们完全有能力来优化它。

小心传统的脚本引入方式带来的性能问题

  现在的Web应用所需的脚本越来越多,一张页面下载几百K的脚本也不再是难以想象的事情了,这就直接导致页面需要更长的时间来加载脚本。不过传统的脚本引入方式(使用<script />)会造成什么问题?再查看这点之前,我们先写一个HttpHandler来模拟一个需要较长时间才能加载的脚本。这很简单,我们只要创建一个Http Handler来做到这一点,如下:

Scripts.ashx
public class Scripts : IHttpHandler {    public void ProcessRequest (HttpContext context) {        context.Response.ContentType = "application/x-javascript";        System.Threading.Thread.Sleep(1500);        context.Response.Write("//");    }    public bool IsReusable {        get {            return false;        }    }}

   我使用Thread.Sleep函数使线程休眠1.5秒,然后输出一个注释符。这样就保证了页面加载该文件需要比较长的时间,也可以将脚本的执行时间降到最低。

  然后我们就写个最简单的页面,来测试一下加载这些文件的结果:

Title
<html xmlns="http://www.w3.org/1999/xhtml" ><head runat="server" id="aaa">    <title>Untitled Page</title>    <script type="text/javascript" language="javascript" src="Scripts.ashx?a"></script>    <script type="text/javascript" language="javascript" src="Scripts.ashx?b"></script>    <script type="text/javascript" language="javascript" src="Scripts.ashx?c"></script>    <script type="text/javascript" language="javascript" src="Scripts.ashx?d"></script>    <script type="text/javascript" language="javascript" src="Scripts.ashx?e"></script></head><body>    ...</body></html>

   在IE里打开页面,看看这些脚本加载的情况。请注意,您可以使用IE Dev Toolbar来禁用Cache(图5)。

图5:IE中传统方式加载脚本的情况

图5:IE中传统方式加载脚本的情况

  真可谓是相当的整齐。不过整齐的背后是较低的性能:脚本文件一个一个被加载,所有脚本文件被加载完需要用8秒多时间。

  那么FireFox的表现又如何?我们使用同样的页面来测试一下(图6)。

图6:FireFox中传统方式加载脚本的情况

图6:FireFox中传统方式加载脚本的情况

  嘿,情况差不多。

  其实出现这个状况是By Design的。从上面这个简单的例子里可能还无法看出,事实上,当浏览器遇到<script />标签时,它会开始加载脚本文件,而此时页面的其它加载行为则会全部停止,包括HTML的呈现,页面或图片的下载等等。这是因为浏览器“怀疑”这些脚本文件中的一些行为可能会再页面中输出HTML。自然,我们可以使用document.write方法这么做。而很多可以放在网站中的第三方小部件,都是靠脚本文件里的document.write方法来生成HTML的。

  这就让用户不太好受了。为什么我的浏览器只能建立一个连接?为什么不能一起下载?我们的带宽不是浪费了很多吗?这些都没错。还记得前一段时间台湾地震使一些Blog无法打开或者打开很慢吗?这很可能就是在页面中使用<script />引入脚本文件时造成的问题:文件下载特别慢,甚至会超时。而且当时我的blog也遇到这个问题。解决方案很简单,把<script />去掉便是。或者,您可以将<script />元素放置在“页尾”代码中,这样,页面就会打开地比较快了——不过当然,那个文件很可能还在继续加载脚本中。

  这就是提高了所谓的“感知性能(Perceived Performance)”,简单的说,就是用户“感受”到的性能。用户会发现页面已经打开了,虽然还没有完全加载完,例如Snap Preview还无法工作。

尝试打破传统脚本引入的瓶颈

  现在的脚本越做越大了,一个200K的文件,如果以20K每秒的速度下载也要10秒。如果这十秒结束之后又来个十秒……这样的网页加载速度太可怕了。我们必须尝试着打破这个瓶颈。

  很有趣的是,如果您在页面中使用document.write来写一个<script />元素的话,这些脚本就可以并行下载了。我们就用下面的代码进行尝试吧:

使用document.write来引入文件
<html xmlns="http://www.w3.org/1999/xhtml" ><head runat="server" id="aaa">    <title>Untitled Page</title>    <script type="text/javascript" language="javascript">        document.write(            '<script type="text/javascript" language="javascript"' +             ' src="Scripts.ashx?a"><' + '/script>');        document.write(            '<script type="text/javascript" language="javascript"' +             ' src="Scripts.ashx?b"><' + '/script>');        document.write(            '<script type="text/javascript" language="javascript"' +             ' src="Scripts.ashx?c"><' + '/script>');        document.write(            '<script type="text/javascript" language="javascript"' +             ' src="Scripts.ashx?d"><' + '/script>');        document.write(            '<script type="text/javascript" language="javascript"' +             ' src="Scripts.ashx?e"><' + '/script>');    </script></head><body>    ...</body></html>

   这样的做法似乎有些复杂,不过应该还算直观。上面代码的目的就是在页面中“写入”<script />元素,以达到引入脚本文件的目的。还是用事实说话,先来看一下IE中打开页面的效果吧(图6):

图7:IE中使用document.write加载脚本的情况

图7:IE中使用document.write加载脚本的情况

  状况好多了。可以看出总是有两个脚本文件在同时下载,虽然还是受制于浏览器对于每个Domain只有2个连接的限制,但是页面加载时间已经从8秒多锐减到不到5秒了。这实在是一个绝好的消息。那么再公布一个好消息,使用这种方式引入脚本文件的话,脚本文件的执行顺序与脚本文件出现的顺序相同。我们只要安排好脚本文件的顺序,这样就可以保证脚本执行的正确性了。

  嘿嘿,不管怎么说这个方法还是非常容易使用的,不是吗?那么让我们欢呼雀跃吧,因为优化就是这么简单!

  很可惜事情的发展并不如我们想象的那么单纯。我们还没有试过FireFox下的状况呢。看了FireFox加载页面的数据统计图,可能就会知道,我们离目标还有很大的距离——因为它的状况和图6的显示状况完全相同,document.write这种做法在FireFox里没有起到任何作用。

  为什么IE的表现和FireFox的表现不同呢?可能这就要问一下浏览器的开发者了,我们现在要做的,可能只是根据结果来为我们的应用想出更好的解决方案。

  路漫漫其修远兮。

三、两个连接还不够“并行”

(原创作品网址:http://www.cnblogs.com/JeffreyZhao/archive/2007/01/22/Break_the_Browsers_Restrictions_3.html)

  在讨论这次的主题之前,我们现在看一下脚本优化的另一个问题,就是“优化难度”。在这里我所说的“优化难度”是指优化一张页面时的修改难度。例如在前一片文章中,使用document.write来引入脚本的话,其“优化难度”会非常的低——没有任何副作用,不用修改其它任何代码。不过它的效果似乎还不太理想,因为仅仅优化了IE下的体验,在FireFox里却没有任何作用。

  很可惜,我回想了几乎所有的优化方式,再也没有找到优化难度如此低的做法了。对于其它的方式,我们都必须在页面的别处进行修改,优化效果越好,修改量越大。对于这些优化方式,我们就必须编写合适的组件,将一些逻辑封装起来。这样可以在一定程度上方便使用,降低优化难度。 

比较document.write与defer

 

  那么这又何document.write或者defer有什么关系?且听我慢慢道来。

  <script />的defer属性在标准里的定义是这样的:

  When set, this boolean attribute provides a hint to the user agent that the script is not going to generate any document content (e.g., no "document.write" in javascript) and thus, the user agent can continue parsing and rendering.

  我们当时遇到JS无法并行下载的原因就是浏览器认为在脚本中可能会输出HTML内容。defer属性的作用就是告诉浏览器,脚本里不会输出任何信息。果然,当我们在IE里使用defer属性时,脚本没有被阻塞,其效果和document.write一样。不过在FireFox里依旧不行,这样的实现实在让人费解。

  都说FireFox标准,看来在细节上也不尽然。

  那么为什么我们在之前使用了document.write而不是defer属性呢?两者效果相同,但是明显使用defer属性更加直观啊。

  defer属性使用起来的确直观和方便。不过,效果真的相同吗?我们可以通过以下的例子试试看。

document.write
<html xmlns="http://www.w3.org/1999/xhtml" ><head runat="server">    <title>Untitled Page</title>    <script type="text/javascript" language="javascript">        document.write(            '<script type="text/javascript" language="javascript"' +             ' src="Scripts.ashx?a"><' + '/script>');        document.write(            '<script type="text/javascript" language="javascript"' +             ' src="Scripts.ashx?b"><' + '/script>');        document.write(            '<script type="text/javascript" language="javascript"' +             ' src="Scripts.ashx?c"><' + '/script>');    </script></head><body>    <input type="button" value="Click" />    <script type="text/javascript" language="javascript" src="Scripts.ashx?a">        alert('Hello World');    </script></body></html>

 

  然后再使用<script defer="defer"></script>的方式引入一下。打开两个页面进行比较就会发现,如果使用document.write的话,在脚本加载完毕之前按钮不会显示,也不会出现提示框;而如果使用defer属性的话,按钮就立即出现了,也会马上出现提示。

  这可麻烦了。如果页面上的元素过早出现,用户在脚本加载完之前进行操作是否会有问题?如果页面里存在直接执行的脚本(如上例的alert调用),在脚本文件加载完之前是否能够执行?如果上面两个问题的答案有任何一个是肯定的话,那么恭喜您,使用defer属性就会造成错误了。而且这个问题的解决方案实在不太容易找到,这大大增加了“优化难度”。

  而且更为关键的是,FireFox同样不支持defer属性的效果。这直接导致了defer属性全面落后于使用document.write的优化方式。既然这样,我们为什么要用它?事实上defer属性用的实在不多,这是个非常典型的的“鸡肋” 特性。

  那么,哪里有使用defer属性的应用呢?我想应该是有的吧,虽然我不知道。

 突破两个连接的限制

  在上一片文章里我们可以看到,虽然document.write方法可以让脚本文件并行加载,但是它依旧受到浏览器的限制。根据HTTP协议的标准,对于同一个Domain,只能同时存在两个连接。在这点上,亲爱的浏览器们都乖乖的实现了。我们如果想要突破这种限制,就要增加域名。不过其实浏览器判断域名的方式是非常严格的,同一域名下的子域名,同一域名不同端口,都不算相同。一般来说,使用子域名来增加并行加载的连接数是比较常用的做法。

  应该已经有不少朋友知道这个方法,它的应用实在太普遍了。不过请注意,请求任意资源时都会建立连接,浏览器对于某一域名的连接并不区分其作用。因此,无论下载图片,CSS文件,JavaScript文件,或者是XMLHttpRequest对象建立的AJAX连接,都属于“两个连接”之内,在优化时往往需要注意这一点。另外,一个浏览器里同时建立的连接数也不是越多越好,根据实验资料显示,浏览器可以同时建立6到7连接最为合适。因此,我们使用3到4个子域名是比较妥当的。

  我们现在就来看一下使用效果。在开发时要出现这个效果,我们可以修改C:/WINDOWS/system32/drivers/etc/hosts文件来设置本地的DNS映射。如下:

在Hosts文件里添加如下映射
127.0.0.1 www.test.com127.0.0.1 sub0.test.com127.0.0.1 sub1.test.com127.0.0.1 sub2.test.com127.0.0.1 sub3.test.com127.0.0.1 sub4.test.com127.0.0.1 sub5.test.com

  我们可以多加一些子域名,方便以后使用。

  接下来我们就可以在页面里从多个不同的子域名加载脚本文件,如下:

从不同子域名加载脚本文件
<html xmlns="http://www.w3.org/1999/xhtml" ><head runat="server">    <title>Untitled Page</title>    <script type="text/javascript" language="javascript">        document.write('<script type="text/javascript" language="javascript"' +             ' src="http://sub0.test.com/Scripts.ashx?a"><' + '/script>');        document.write('<script type="text/javascript" language="javascript"' +             ' src="http://sub0.test.com/Scripts.ashx?b"><' + '/script>');        document.write('<script type="text/javascript" language="javascript"' +             ' src="http://sub1.test.com/Scripts.ashx?c"><' + '/script>');        document.write('<script type="text/javascript" language="javascript"' +             ' src="http://sub1.test.com/Scripts.ashx?d"><' + '/script>');        document.write('<script type="text/javascript" language="javascript"' +             ' src="http://sub2.test.com/Scripts.ashx?e"><' + '/script>');    </script></head><body>    ...</body></html>

  在浏览器打开页面试试看?还记得当初我们加载页面用了多少时间吗?8秒多!而现在已经能够在不到2秒内加载完毕了(如图2)。

图8:使用多个子域名进行并行加载

图8:使用多个子域名进行并行加载

  可惜我们还要优化FireFox浏览器里的情况,下次我们就来讨论这个问题。接下来的优化方案会有一定的难度,不过只要我们利用得当,将会大大提高Perceived Performance。

四、王道!动态添加Script元素

(原创作品网址:http://www.cnblogs.com/JeffreyZhao/archive/2007/01/25/Break_the_Browsers_Restrictions_4.html)

  我们已经知道,脚本文件的并行下载能够提高页面的加载速度。但是目前还有一个急需解决的问题,那就是对于FireFox浏览器的优化。在我们之前使用的优化方法,无论是简单实用的document.write还是食之无味的defer属性,FireFox浏览器都对此置若罔闻。不过FireFox也不是绝对地“冥顽不灵”,开发人员还是有方法对它进行优化的。

  这个方法就是动态添加script元素。

动态添加script元素

  不知道“动态添加script元素”这个说法是否正确,我在这里的意思是使用JavaScript编程,向<head />里添加script元素。下面的代码动态添加了5个script元素:

动态添加script元素
<html xmlns="http://www.w3.org/1999/xhtml"><head runat="server" id="head">    <title>Untitled Page</title>    <script type="text/javascript" language="javascript">        for (var i = 0; i < 5; i ++)        {            var script = document.createElement("script");            script.type = "text/javascript";            script.src = "Script.ashx?a=" + i;            document.getElementById('head').appendChild(script);        }    </script>    </head><body>    ...</body></html>

  请注意,由于在JavaScript代码执行时页面还没有加载完毕,因此还不能使用document.getElementsByTagName方法来获得head元素,我们只能为head元素添加一个id,并使用document.getElementById方法来获得它。打开这张页面,就会发现,无论是IE(图9)还是FireFox(图10)的元素加载都会发现优化的效果:

图9:IE中动态加载script元素效果

图9:IE中动态加载script元素效果

图10:FireFox中动态加载script元素效果

图10:FireFox中动态加载script元素效果

  我们姑且不关心为什么FireFox中每个脚本文件会使用2.5秒进行加载,但是并行加载的效果切切实实的出现了!加上多域名,效果更明显。

  细心的朋友不知道回想起什么了吗?没错,当年在ASP.NET AJAX某个版本中(我记得是Beta 1,有些模糊了)加载自定义脚本时使用了Sys.Application.queueScriptReference方法,它能够让脚本文件并行加载。但是由于接下来会谈到的几个问题,最终还是选择了传统的加载方式。不过ASP.NET AJAX还是细心地考虑到脚本加载的影响,ScriptManager和ScriptReference已经提供了LoadScriptsBeforeUI属性,我们现在就能够控制script元素是出现在UI之前还是之后了,我们可以影响性能但是无需“急用”的脚本放在所有UI的最后进行加载,以降低它对于性能的影响(这个是在刚发布的ASP.NET AJAX正式版中新增的功能,我在阅读代码时无意发现)。

  再说句题外话,虽然这个脚本加载方法已经被取消了,但是功能依旧存在,因为UpdatePanel在Partial Rendering之后只能选择动态加载脚本文件。我们也能够自己使用这样的加载方式,然而这就超出了今天这篇文章讨论的范围。不过既然ASP.NET AJAX正式版已经发布了,我也能够放心的继续《深入Atlas系列》了。:)

动态添加script元素的缺陷

  世界上很少有完美的事物。动态添加的script元素能够使IE和FireFox里都得到优化,它应该也会有些麻烦,否则为什么这个方法没有被推广呢?

  而且事实上,动态添加script元素的做法是“优化难度”最高的方法。我现在就来一一列举这些“缺陷”:

(1)、无法阻碍页面加载

  其实这个问题和在IE中使用defer属性遇到的问题相同。如果您需要在页面中直接使用脚本文件里定义的函数,就不能轻易使用这个做法。即使它的确能够优化页面的加载速度。

(2)、影响window.onload事件的触发

  如果对于window.onload事件的处罚有所影响,但是这种影响能够在不同浏览器中得到统一倒也罢了,还相对容易处理一些。现在的问题就在于,在IE中,window.onload事件会在页面其它元素被加载完毕之后立即触发,而FireFox里的window.onload事件会等待动态添加的那些脚本文件也被加载完毕后才触发。虽然我们开发人员是伟大的,可是要兼容这两种情况依旧不是一件易如反掌的事情。

(3)、动态加载脚本的执行顺序

  这一点才是最致命的。

  虽然我们动态加载的script元素是有严格顺序的,但是浏览器可不一定这样认为。在FireFox中,脚本文件会按照它动态加载的script元素的顺序执行,而IE会根据脚本文件下载完毕的顺序执行。

  那还得了?

那么为何称之为王道?

  既然麻烦这么多,为什么还称之为“王道”?其实我们只要合理的使用这个方法,就能够大大提高页面的Perceived Performance。

  可能在这里我需要重新定义一下“Perceived Performance”的概念。它的意思是“用户感受到的性能”。我们打开一个页面,例如Windows Live个人主页,会发现页面的框架都被加载了,但是每一个框架都是Loading状态。然后每一个模块陆陆续续地加载成功。

  我们来想象一下这个场景。一个页面的所有内容(包括模块),需要20秒钟才能加载完毕。但是它用了10秒钟就显示出了模块的框架,在接下来10秒钟内每个模块慢慢的出现。还有一种情况,就是等待整整20秒才能看到页面。从用户角度来看,哪个性能比较高呢?

  这个就是Perceived Performance的经典应用。从所谓的Web 2.0开始,Perceived Peformance的重要性可以说被提高到了一个前所未有的高度。

  那么我们现在就用语言来简单描述一下应该如何实现这样的效果:

  1. 首先,在页面上用传统方式(最好使用document.write)加载所需要的基础脚本以及所有的HTML,这时候所有的模块处于Loading状态。
  2. 在window.onload事件被触发后,动态加载每个模块所需的脚本。我们只需要在IE浏览器中响应script元素的onload事件或者在其它浏览器中响应script元素的onreadystatechange事件,就可以捕捉脚本文件的加载情况。
  3. 在上述事件的handler中,如果script元素的readyState为"complete"或"loaded"(script元素的readyState使用字符串表示),那么判断某个模块需要的脚本有没有加载完毕,如果完毕了,则显示那个模块的具体内容。

  大体方式就是这样,逻辑非常简单,不过在编码上可能就会遇到一些问题。不过对于使用ASP.NET AJAX的开发人员可能就略有福气些了,因为ASP.NET AJAX内置就有动态添加脚本元素的机制,已经实现了很好的跨浏览器特性。它们能够稍稍便于我们的开发,有机会我将详细的介绍它们,并且和大家一起来设计和实现一个好用的脚本库。

  我对于加载脚本文件的优化心得就只有这些了,不过我们还可以在其他方面进行优化。例如,AJAX应用里最常见的XMLHttpRequest对象,我们也可以有技巧地使用它。不过这些内容,就要等下次再和大家分享了。:)

五、哭笑不得的IE Bug

(原创作品网址:http://www.cnblogs.com/JeffreyZhao/archive/2007/01/27/Break_the_Browsers_Restrictions_5.html)

  还记得《ASP.NET AJAX Under the Hood Secrets》吗?这是我在自己的Blog上推荐过的唯一一篇文章(不过更可能是一时兴起)。在这片文章里,Omar Al Zabir提出了他在使用ASP.NET AJAX中的一些经验。其中提到的一点就是:Browsers do not respond when more than two calls are in queue。简单的说,就是在IE中,如果同时建立了超过2两个连接在“连接状态”中,但是没有连接成功(连接成功之后就没有问题了,即使在传输数据),浏览器会停止对其他操作的响应,例如点击超级链接进行页面跳转,直到除了正在尝试的两个连接就没有其他连接时,浏览器才会重新响应用户操作。

  出现这个问题一般需要3个条件:

  • 同时建立太多连接,例如一个门户上有许多个模块,它们在同时请求服务器端数据。
  • 响应比较慢,从浏览器发起连接,到服务器端响应连接,所花的时间比较长。
  • 使用IE浏览器,无论IE6还是IE7都会这个问题,而FireFox则一切正常。

  在IE7里居然还有这个bug,真是令人哭笑不得。但是我们必须解决这个问题,不是吗?

编写代码来维护一个队列

  与《ASP.NET AJAX Under the Hood Secrets》一文中一样,最容易想到的解决方案就是编写代码来维护一个队列。这个队列非常容易编写,代码如下:

RequestQueue.js
if (!window.Global){    window.Global = new Object();}Global._RequestQueue = function(){    this._requestDelegateQueue = new Array();        this._requestInProgress = 0;        this._maxConcurrentRequest = 2;}Global._RequestQueue.prototype ={    enqueueRequestDelegate : function(requestDelegate)    {        this._requestDelegateQueue.push(requestDelegate);        this._request();    },        next : function()    {        this._requestInProgress --;        this._request();    },        _request : function()    {        if (this._requestDelegateQueue.length <= 0) return;        if (this._requestInProgress >= this._maxConcurrentRequest) return;                this._requestInProgress ++;        var requestDelegate = this._requestDelegateQueue.shift();        requestDelegate.call(null);    }}Global.RequestQueue = new Global._RequestQueue();

  我在实现这个队列时使用了最基本的JavaScript,可以让这个实现不依赖于任何AJAX类库。这个实现非常容易实现的,我简单介绍一下它的使用方式。

  1. 在需要发起AJAX请求时,不能直接调用最后的方法来发起请求。需要封装一个delegate然后放入队列。
  2. 在AJAX请求完成时,调用next方法,可以发起队列中的其他请求。

  例如,我们在使用prototype 1.4.0版时我们可以这样:

使用Prototype 1.4.0和Request Queue
<html xmlns="http://www.w3.org/1999/xhtml" ><head>    <title>Request Queue</title>    <script type="text/javascript" src="js/prototype-1.4.0.js"></script>    <script type="text/javascript" src="js/RequestQueue.js"></script>        <script language="javascript" type="text/javascript">        function requestWithoutQueue()        {            for (var i = 0; i < 10; i++)            {                new Ajax.Request(                    url,                    {                        method: 'post',                        onComplete: callback                    });            }                        function callback(xmlHttpRequest)            {                ...            }        }                function requestWithQueue()        {            for (var i = 0; i < 10; i++)            {                var requestDelegate = function()                {                    new Ajax.Request(                        url,                        {                            method: 'post',                            onComplete: callback,                            onFailure: Global.RequestQueue.next,                            onException: Global.RequestQueue.next                        });                }                                Global.RequestQueue.enqueueRequestDelegate(requestDelegate);            }                        function callback(xmlHttpRequest)            {                ...                Global.RequestQueue.next();            }        }    </script></head><body>    ...</body></html>

  在上面的代码中,requestWithoutQueue方法发起了普通的请求,requestWithQueue则使用了Request Queue,大家可以比较一下它们的区别。

使用Request Queue的缺陷

  这个Request Queue能够工作正常,但是使用起来实在不方便。为什么?

  我们来想一下,如果一个应用已经写的差不多了,我们现在需要在页面里使用这个Request Queue,我们需要怎么做?我们需要修改所有发起请求的地方,改成使用Request Queue的代码,也就是建立一个Request Delegate。而且,我们需要把握所有的异常情况,保证在出现错误时,Global.RequestQueue.next方法也能够被及时地调用。否则这个队列就无法正常工作了。还有,ASP.NET AJAX中有UpdatePanel,该怎么建立Request Delegate?该如何访问Global.RequestQueue.next方法?

  我们该怎么办?

可怜的JavaScript,太容易受骗了

  我们需要找出一种方式,能够轻易的用在已有的应用中,解决已有应用中的问题。怎么样才能让已有应用修改尽可能的少呢?我们来想一个最极端的情况:一行代码都不用改,这可能么?

  似乎是可能的,我们只需要骗过JavaScript就可以。可怜的JavaScript,太容易骗了。话不多说,直接来看代码,一目了然:

FakeXMLHttpRequest.js
window._progIDs = [ 'Msxml2.XMLHTTP', 'Microsoft.XMLHTTP' ];if (!window.XMLHttpRequest){    window.XMLHttpRequest = function()    {        for (var i = 0; i < window._progIDs.length; i++)        {            try            {                var xmlHttp = new _originalActiveXObject(window._progIDs[i]);                return xmlHttp;            }            catch (ex) {}        }                return null;    }}if (window.ActiveXObject){        window._originalActiveXObject = window.ActiveXObject;    window.ActiveXObject = function(id)    {        id = id.toUpperCase();                for (var i = 0; i < window._progIDs.length; i++)        {            if (id === window._progIDs[i].toUpperCase())            {                return new XMLHttpRequest();            }        }                return new _originaActiveXObject(id);    }}window._originalXMLHttpRequest = window.XMLHttpRequest;window.XMLHttpRequest = function(){    this._xmlHttpRequest = new _originalXMLHttpRequest();    this.readyState = this._xmlHttpRequest.readyState;    this._xmlHttpRequest.onreadystatechange =         this._createDelegate(this, this._internalOnReadyStateChange);}window.XMLHttpRequest.prototype = {    open : function(method, url, async)    {        this._xmlHttpRequest.open(method, url, async);        this.readyState = this._xmlHttpRequest.readyState;    },        send : function(body)    {        var requestDelegate = this._createDelegate(            this,            function()            {                this._xmlHttpRequest.send(body);                this.readyState = this._xmlHttpRequest.readyState;            });                Global.RequestQueue.enqueueRequestDelegate(requestDelegate);    },        setRequestHeader : function(header, value)    {        this._xmlHttpRequest.setRequestHeader(header, value);    },        getResponseHeader : function(header)    {        return this._xmlHttpRequest.getResponseHeader(header);    },        getAllResponseHeaders : function()    {        return this._xmlHttpRequest.getAllResponseHeaders();    },        abort : function()    {        this._xmlHttpRequest.abort();    },        _internalOnReadyStateChange : function()    {        var xmlHttpRequest = this._xmlHttpRequest;                try        {            this.readyState = xmlHttpRequest.readyState;            this.responseText = xmlHttpRequest.responseText;            this.responseXML = xmlHttpRequest.responseXML;            this.statusText = xmlHttpRequest.statusText;            this.status = xmlHttpRequest.status;        }        catch(e){}                if (4 === this.readyState)        {            Global.RequestQueue.next();        }                if (this.onreadystatechange)        {            this.onreadystatechange.call(null);        }    },        _createDelegate : function(instance, method)    {        return function()        {            return method.apply(instance, arguments);        }    }}

  本来在想出这个解决方案时,我心中还比较忐忑,担心这个方法的可行性。当真正完成时,可真是欣喜不已。这个解决方案的的关键就在于“伪造JavaScript对象”。JavaScript只会直接根据代码来使用对象,我们如果将一些原生对象保留起来,并且提供一个同名的对象。这样,JavaScript就会使用你提供的伪造的JavaScript对象了。在上面的代码中,主要伪造了两个对象:

  • window.XMLHttpRequest对象:我们将XMLHttpRequest原生对象保留为window._originalXMLHttpRequest,并且提供一个新的(或者说是伪造的)window.XMLHttpRequest类型。在新的XMLHttpRequest对象中,我们封装了一个原生的XMLHttpRequest对象,同时也会定义了XMLHttpRequest原生对象存在的所有方法和属性,大多数的方法都会委托给原生XMLHttpRequest对象(例如abort方法)。需要注意的是,我们在新的XMLHttpRequest类型的send方法中,创造了一个delegate放入了队列中,并且_internalOnReadyStateChange方法在合适的情况下(readyState为4,表示completed)调用Global.RequestQueue.next方法,然后再触发onreadystatechange的handler。
  • ActiveXObject对象:由于类库在创建XMLHttpRequest对象的实现不同,有的类库会首先使用ActiveX进行尝试(例如prototype),有些则会首先尝试window.XMLHttpRequest对象(例如Yahoo! UI Library),因此我们必须保证在通过ActiveX创建XMLHttpRequest对象时也能够使用我们伪造的window.XMLHttpRequest类。实现相当的简单:保留原有的window.ActiveXObject对象,在通过新的window.ActiveXObject创建对象时判断传入的id是否为XMLHttpRequest所需的id,如果是,则返回伪造的window.XMLHttpRequest对象,否则则使用原来的ActiveXObject(保存在window._originaActiveXObject变量里)创建所需的ActiveX控件。

  其实“骗取”JavaScript的“信任”非常简单,这也就是JavaScript灵活的体现,我们在扩展一个JS类库时,我们完全可以想一下,是否能够使用一些“巧妙”的办法来改变原有的逻辑呢?

“伪造”XMLHttpRequest对象的优点与缺点

  现在,要在已有的应用中修改浏览器僵死的状况则太容易了,只需在IE浏览器中引入RequestQueue.js和FakeXMLHttpRequest.js即可。而且我们只需要把“判断”浏览器类型的任务交给浏览器本身就行了,如下:

实现一个队列
<!--[if IE]>    <script type="text/javascript" src="js/RequestQueue.js"></script>    <script type="text/javascript" src="js/FakeXMLHttpRequest.js"></script><![endif]-->

  这样,只有在IE浏览器中,这两个文件才会被下载,何其容易!

  那么,这么做会有什么缺点呢?可能最大的缺点,就是伪造的对象无法完全模拟XMLHttpRequest的“行为”。如果在服务器完全无法响应时,访问XMLHttpRequest的status则会抛出异常。请注意,这里说的“完全无法响应”不是指Service Unavailable(很明显,它的status是503),而是彻底的访问不到,比如机器的网络连接断了。而在伪造的XMLHttpRequest中,status无法模拟一个方法调用(IE没有FireFox里的__setter__),因此无法抛出异常。

  这个问题很严重吗?个人认为没有什么问题。看看常见的类库封装,都是直接访问status,而不会判断它到底会不会出错。这也说明,这个状况本身已经被那些类库所忽略了。

  那么我们也忽略一下吧,这个解决方案还是比较让人满意的。至少目前看来,在使用过程中没有出现问题。我们的“欺骗”行为没有被揭穿,异常成功。:)

六、AJAX也跨域名

(原创作品网址:http://www.cnblogs.com/JeffreyZhao/archive/2007/02/02/Break_the_Browsers_Restrictions_6.html)

  标题有些唬人的成分,因为这里跨的只是子域名。

  事情的经过是这样的,还是那个个人门户网站。其中有个功能就是RSS订阅,每个订阅作为一个模块出现在页面上。如果一个用户订阅了比较多的RSS,则在打开页面时所有的RSS模块就会开始加载,这时候可能就会需要十几秒甚至更长的时间才能加载完毕。这时,如果用户需要作别的AJAX操作——比如保存页面设置——那么长时间的等待就不可避免了,谁让浏览器对于相同域名只能同时存在两个连接呢?不过这可不是一个好的用户体验,那么我们需要怎么做呢?

  第一种做法可能比较容易想到,我们可以自己编写代码维护一个Priority Queue,为每个请求附加一个“优先级”信息,这样我们就可以把重要的请求率先发出。这样就可以在一定程度上解决用户的等待问题。可惜这个方法还是无法突破两个连接的限制。于是第二种做法,我们就要设法突破两个连接的限制了。如果能够向别的域名发出AJAX请求,不也就能避免重要的请求被大量的请求所阻塞了吗?

  我们还是从头看起,一点一点地来解决这个问题。

阻塞的AJAX请求

  我们先来证实一下请求的阻塞情况吧。我们使用如下的代码:

连续发起三个请求
function simpleRequest(){    var request = new XMLHttpRequest();    request.open("POST", "Script.ashx");    request.send(null);}function threeRequests(){    simpleRequest();    simpleRequest();    simpleRequest();}

  当执行threeRequests时就会连续发出3个相同域名的请求,还是通过统计图表来查看阻塞的效果(如图11):

图11:最后的请求被前两个请求阻塞

图11:最后的请求被前两个请求阻塞

  每个请求需要花费1.5秒的时间。很明显,第三个请求必须等到第一个请求结束之后才能执行,因此总共需要进行3秒多钟才能执行完毕。我们要改变的就是这个状况。

传统的跨域名异步请求解决方案

  AJAX安全性的唯一保证,似乎就是对于跨域名(Cross-Domain)AJAX请求的限制。除非打开本地硬盘的网页,或者在IE中将跨域名传输数据的限制打开,否则向其他域名发出AJAX请求都会被禁止。而且对于跨域名的判断非常严格,不同的子域名,或者相同域名的不同端口,都会被认作是不同的域名,我们不能向它们的资源发出AJAX请求。

  从表面上看起来似乎没有办法打破这个限制,还好我们有个救星,那就是iframe!

  iframe虽然不在标准中出现,但是由于它实在有用,FireFox也“不得不”对它进行了支持(类似的还有innerHTML)。网上已经有一些跨域名发出异步请求的做法,但是它们实在做的不好。它们的简单工作原理如下:在另一个域名下放置一个特定的页面文件作为Proxy,主页面将异步请求的信息通过Query String传递入iframe里的Proxy页面,Proxy页面在AJAX请求执行完毕后将结果放在自己location的hash中,而主页面会对iframe的src的hash值进行轮询,一旦发现它出现了改变,则通过hash值得到需要的信息。

  这个方法的实现比较复杂,而且功能有限。在IE和FireFox中,对于URL的长度大约可以支持2000个左右的字符。对于普通的需求它可能已经足够了,可惜如果真要传递大量的数据,这就远远不够了。与我们一会儿要提出的解决方案相比,可能它唯一的优势就是能够跨任意域名进行异步请求,而我们的解决方案只能突破子域名的限制。

  那么现在来看看我们的做法!

优雅地突破子域名的限制

  我们突破子域名限制的关键还是在于iframe。

  iframe是的好东西,我们能够跨过子域名来访问iframe里的页面对象,例如window和DOM结构,包括调用JavaScript(通过window对象)——我们将内外页面的document.domain设为相同就可以了。然后在不同子域名的页面发起不同的请求,把结果通过JavaScript进行传递即可。唯一需要的也仅仅是一个简单的静态页面作为Proxy而已。

  我们现在就来开始编写一个原形,虽然简单,但是可以说明问题。

  首先,我们先来编写一个静态页面,作为放在iframe里的Proxy,如下:

SubDomainProxy.html
<html xmlns="http://www.w3.org/1999/xhtml" ><head>    <title>Untitled Page</title>    <script type="text/javascript" language="javascript">        document.domain = "test.com";                function sendRequest(method, url)        {            var request = new XMLHttpRequest();            request.open(method, url);            request.send(null);        }    </script></head><body></body></html>

  然后我们再编写我们的主页面:

http://www.test.com/Default.html
<html xmlns="http://www.w3.org/1999/xhtml" ><head runat="server">    <title>Untitled Page</title>    <script type="text/javascript" language="javascript">        document.domain = "test.com";            function simpleRequest()        {            var request = new XMLHttpRequest();            request.open("POST", "Script.ashx");            request.send(null);        }                function crossSubDomainRequest()        {            var proxy = document.getElementById("iframeProxy").contentWindow;            proxy.sendRequest('POST', 'http://sub0.test.com/Script.ashx');        }                function threeRequests()        {            simpleRequest();            simpleRequest();            crossSubDomainRequest();        }    </script></head><body>    <input type="button" value="Request" onclick="threeRequests()" />    <iframe src="http://sub0.test.com/SubDomainProxy.html" style="display:none;"         id="iframeProxy"></iframe></body></html>

  当执行threeRequests方法时,将会同时请求http://www.test.com以及http://sub0.test.com两个不同域名下的资源。很明显,最后一个请求已经不会受到前两个请求的阻塞了(如图12):

图12:不同域名的请求不会被阻塞

图12:不同域名的请求不会被阻塞

  令人满意的结果!

  虽说只能突破子域名,但是这已经足够了,不是吗?我们为什么要强求任意域名之间能够异步通讯呢?更何况我们的解决方案是多么的优雅!在下一篇文章中,我们将会为ASP.NET AJAX客户端实现一个完整的CrossSubDomainRequestExecutor,它会自动判断是否正在发出跨子域名的请求,并选择AJAX请求的方式。这样,客户端的异步通讯层就会对开发人员完全透明。世上还会有比这更令人愉快的事情吗?:)

注意事项

  可能以下几点值得一提:

  • 我在出现这个想法之后也作了一些尝试,最后发现创建XMLHttpRequest对象,调用open方法和send方法都必须在iframe中的页面中执行才能够在IE和FireFox中成功发送AJAX请求。
  • 在上面的例子中,我们向子域名请求的的路径是http://sub0.test.com/Script.ashx。请注意,完整的子域名不可以省略,否则在FireFox下就会出现权限不够的错误,在调用open方法时就会抛出异常——似乎FireFox把它当作了父页面域名的资源了。
  • Windows Live Contacts Gadget使用了一种叫做Channel的技术,用于解决跨任意域名传递数据的问题,我相当佩服微软技术人员的创造力。Channel技术是一种优秀的解决跨域名异步请求问题的解决方案,而且如果将它封装成了组件,那么使用起来也会相当优雅(似乎微软已经准备这么做了)。不过它和我们现在需要解决的问题并不相同,如果有机会的话,我也会详细的解释一下Channel技术——但不是现在,因为我觉得我还没有完全理解这个技术本身。

七、CrossSubDomainExecutor

(原创作品网址:http://www.cnblogs.com/JeffreyZhao/archive/2007/02/05/Break_the_Browsers_Restrictions_7.html)

  在上次的文章中,我们已经提到了一种能够跨子域名进行AJAX请求的方法。我们现在就来实现一个对开发人员透明的实现,它会自动判断这个请求是否是跨子域名,如果不是,则使用传统的方法发出AJAX请求,反之则使用我们的方式。

  我在如何实现这个Executor的问题上,我想了很久。按照ASP.NET AJAX的“标准”来说,应该开发一个WebRequestExecutor的子类,然后将其设为默认的Executor或者某个特定WebRequest的Executor。但是最终我使用了直接修改XMLHttpExecutor的做法,因为考虑到以下原因:

  • 完整实现一个WebRequestExecutor需要实现太多接口,而CrossSubDomainExecutor和XMLHttpExecutor有太多相同的地方。
  • 如果继承WebRequestExecutor的话,还是需要了解XMLHttpExecutor的太多细节,JavaScript做不到太好的封装,无法实现真正的面向对象。
  • CrossSubDomainExecutor在普通情况下和XMLHttpExecutor的行为一模一样。

  直接修改XMLHttpExecutor的做法其实就是在XMLHttpExecutor的prototype上做文章,这也就是JavaScript扩展对象的一贯做法。

  首先,我们定义一个Sys.Net.WebRequest类的静态方法,用来从一个URL中获得域名。例如输入http://www.sample.com/Default.aspx这个URL,返回http://www.sample.com,这个静态函数对于判断是否Cross Domain非常重要,如下: 

Sys.Net.WebRequest._getRawDomain静态方法
Sys.Net.WebRequest._getRawDomain = function(url){    url = Sys.Net.WebRequest._resolveUrl(url);        var index = url.indexOf('://');    var prefix = url.substring(0, index + 3);        var url = url.substring(index + 3);    index = url.indexOf('/');    if (index < 0)    {        index = url.indexOf('?');    }        if (index >= 0)    {        url = url.substring(0, index);    }        return prefix + url;}

  其次,我们为Sys.Net.WebRequest类扩展一个,用于检测请求能否直接发出,而不需要使用我们的方法。在这里我们直接使用了一个XMLHttpRequest对象作为尝试:

Sys.Net.WebRequest._checkIfIsCrossDomainRequest方法
Sys.Net.WebRequest.prototype._checkIfIsCrossDomainRequest = function(){    var request = new XMLHttpRequest();    try    {        request.open('get', this.get_url());        return false;    }    catch(e)    {        return true;    }}

  我们还需要得到WebRequest的两个特性:它的Document Domain(设为document.domain的值),以及它本身的域名(Raw Domain),我们依旧为Sys.Net.Request扩展方法:

Sys.Net.WebRequest._getDocDomain方法
Sys.Net.WebRequest.prototype._getDocDomain = function(){    if (!this._docDomain)    {        var pageDomain = Sys.Net.WebRequest._getRawDomain(window.location.href);        var requestDomain = this._getRawDomain();                var i = 0;        while (true)        {            i ++;            var c1 = pageDomain.charAt(pageDomain.length - i);            var c2 = requestDomain.charAt(requestDomain.length - i);                        if (c1 !== c2)            {                break;            }        }                var url = pageDomain.substring(pageDomain.length - i + 1);        var index = url.indexOf('.');        this._docDomain = url.substring(index + 1);    }        return this._docDomain;}

 

Sys.Net.WebRequest._getRawDomain方法
Sys.Net.WebRequest.prototype._getRawDomain = function(){    if (!this._rawDomain)    {        this._rawDomain = Sys.Net.WebRequest._getRawDomain(this.get_url());    }        return this._rawDomain;}

  我们要修改XMLHttpExecutor,最关键的一步就是要重新实现executeRequest方法。很显然,再这之前,我们需要保留原来的实现:

保留原来的executeRequest方法,并提供新的实现
Sys.Net.XMLHttpExecutor.prototype._normalExecuteRequest =     Sys.Net.XMLHttpExecutor.prototype.executeRequest;Sys.Net.XMLHttpExecutor.prototype.executeRequest = function(){    if (this.get_webRequest()._checkIfIsCrossDomainRequest())    {        this._crossDomainExecuteRequest();    }    else    {        this._normalExecuteRequest();    }}

  接下来,就需要实现一个跨子域名发出AJAX请求方法了。再这之前,需要对于XMLHttpExecute本身的实现有所了解。我们现在就对添加的方法进行简单的分析。

  首先,自然是_crossDomainExecuteRequest方法。我们准备了两个字典:_iframeCache和_iframeLoaded,分别用于保存作为Proxy的iframe对象,以及表明iframe对象是否已经加载成功了(iframe加载也是异步的,也需要一定时间)。我们先模仿XMLHttpExecutor的实现,建立一个侦测是否超时的监听器;再使用该请求的Raw Domain作为key,经过下面判断的三个分支做出不同逻辑。它们是:

  1. 如果iframeCache中没有相应的iframe对象,则调用_createIFrame方法创建一个作为Proxy的iframe。
  2. 如果已经存在iframe对象,但是Proxy页面还没有加载完毕,则等待500毫秒后重新尝试着调用_crossDomainExecuteRequest方法。
  3. 如果Proxy已经加载成功了,并且用户没有调用abort方法,请求也没有过超时时间,则将document.domain设为正确值,并且调用Proxy页面里的方法发出AJAX请求。
Sys.Net.XMLHttpExecutor._crossDomainExecuteRequest方法
Sys.Net.XMLHttpExecutor._iframeCache = {};Sys.Net.XMLHttpExecutor._iframeLoaded = {};Sys.Net.XMLHttpExecutor.prototype._crossDomainExecuteRequest = function(){    var webRequest = this.get_webRequest();    var rawDomain = webRequest._getRawDomain();    var iframeCache = Sys.Net.XMLHttpExecutor._iframeCache;    var timeout = webRequest.get_timeout();    if (timeout > 0)    {        this._timer = window.setTimeout(this._onTimeout, timeout);    }        if (!iframeCache[rawDomain])    {        this._createIFrame();    }    else if (!Sys.Net.XMLHttpExecutor._iframeLoaded[rawDomain])    {        setTimeout(            Function.createDelegate(this, this._crossDomainExecuteRequest),            500);    }    else if (!this.get_aborted() && !this.get_timedOut())    {        document.domain = webRequest._getDocDomain();        iframeCache[rawDomain].contentWindow.sendRequest(this);    }}

  createIFrame方法的作用就是创建一个新的作为Proxy的iframe对象,加载的内容即为我们准备的Proxy页面,请注意Proxy页面还带有QueryString,用于指定Proxy页面的document.domain。我们也会响应iframe对象的onload事件。很幸运,这个事件被IE和FireFox浏览器都实现了——毕竟FireFox原本就是向IE拿来的iframe,有何道理实现的不同呢?

Sys.Net.XMLHttpExecutor._createIFrame方法
Sys.Net.XMLHttpExecutor.prototype._createIFrame = function(){    var webRequest = this.get_webRequest();    var rawDomain = webRequest._getRawDomain();    var proxyUrl = rawDomain + "/SubDomainProxy.htm?" + 
webRequest._getDocDomain(); var iframe = document.createElement('iframe'); Sys.Net.XMLHttpExecutor._iframeCache[rawDomain] = iframe; iframe.style.display = "none"; $addHandler(iframe, 'load',
Function.createDelegate(this, this._onIFrameLoadHandler)); iframe.src = proxyUrl; document.body.appendChild(iframe); return iframe;}

  在iframe加载成功时,onIFrameLoadHandler方法会被调用。在这个方法里,我们将会通过查看Proxy方法有没有被正确加载来判断iframe里的Proxy有没有被加载成功。如果没有加载成功,则清除iframeCache字典中相应的iframe对象,并直接结束WebRequest请求:清除检测超时的Timer,调用WebRequest对象的completed方法等等。如果加载成功了,则在iframeLoaded字典中标记这个iframe已经加载成功了,并且在该方法之后重新调用_crossDomainExecuteRequest方法——只要使用setTimeout再将时间设为0即可:

Sys.Net.XMLHttpExecutor._onIFrameLoadHandler方法
Sys.Net.XMLHttpExecutor.prototype._onIFrameLoadHandler = function(){    var webRequest = this.get_webRequest();    var rawDomain = webRequest._getRawDomain();    var iframeCache = Sys.Net.XMLHttpExecutor._iframeCache;        try    {        document.domain = webRequest._getDocDomain();        if (iframeCache[rawDomain].contentWindow.sendRequest)        {            Sys.Net.XMLHttpExecutor._iframeLoaded[rawDomain] = true;            setTimeout(                Function.createDelegate(this, this._crossDomainExecuteRequest),                0);        }        else        {            throw new Error();        }    }    catch(e)    {        var iframe = iframeCache[rawDomain];        document.body.removeChild(iframe);        iframeCache[rawDomain] = null;        this._clearTimer();        webRequest.completed(Sys.EventArgs.Empty);    }}

  最后我们只要再提供一个作为Proxy的页面即可,我们必须把它放在每个子域名的根目录下——当然您也可以改进之前_createIFrame的代码,加载其他位置上的Proxy页面。Proxy页面的实现非常简单,只要模仿XMLHttpExecutor原有的executeRequest方法实现即可:

SubDomainProxy.htm页面
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml" ><head>    <title>Untitled Page</title>    <script type="text/javascript" language="javascript">        var search = window.location.search;        document.domain = search.substring(1);                if (!window.XMLHttpRequest)        {            window.XMLHttpRequest = function window$XMLHttpRequest()            {                var progIDs = [ 'Msxml2.XMLHTTP', 'Microsoft.XMLHTTP' ];                                for (var i = 0; i < progIDs.length; i++)                {                    try                     {                        var xmlHttp = new ActiveXObject(progIDs[i]);                        return xmlHttp;                    }                    catch (ex) {}                }                                return null;            }        }                function sendRequest(executor)        {            executor._webRequest = executor.get_webRequest();            if (executor._started)            {                throw window.parent.Error.invalidOperation(                    window.parent.String.format(                        window.parent.Sys.Res.cannotCallOnceStarted, 
'executeRequest')); } if (executor._webRequest === null) { throw window.parent.Error.invalidOperation( window.parent.Sys.Res.nullWebRequest); } var body = executor._webRequest.get_body(); var headers = executor._webRequest.get_headers();
executor._xmlHttpRequest = new XMLHttpRequest(); executor._xmlHttpRequest.onreadystatechange =
executor._onReadyStateChange;
var verb = executor._webRequest.get_httpVerb(); executor._xmlHttpRequest.open(verb,
executor._webRequest.getResolvedUrl(), true); if (headers) { for (var header in headers) { var val = headers[header]; if (typeof(val) !== "function") executor._xmlHttpRequest.setRequestHeader(header, val); } } if (verb.toLowerCase() === "post") { if ((headers === null) || !headers['Content-Type']) { executor._xmlHttpRequest.setRequestHeader(
'Content-Type', 'application/x-www-form-urlencoded'); } if (!body) { body = ""; } } executor._xmlHttpRequest.send(body); executor._started = true; } </script></head><body></body></html>

  到目前为止,这个CrossDomainExecutor就实现好了,我们现在使用WebRequst时就可以把它的URL设为不同子域名下的资源。例如,我们在http://www.test.com里可以请求http://sub.test.comhttp://sub0.sub1.test.com里的资源。而且,如果不做跨子域名的请求,XMLHttpExecutor的行为和之前不会有任何不同。

  点击这里下载源码。

原创粉丝点击