还记得《
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类库。这个实现非常容易实现的,我简单介绍一下它的使用方式。
在需要发起AJAX请求时,不能直接调用最后的方法来发起请求。需要封装一个delegate然后放入队列。
在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,而不会判断它到底会不会出错。这也说明,这个状况本身已经被那些类库所忽略了。
那么我们也忽略一下吧,这个解决方案还是比较让人满意的。至少目前看来,在使用过程中没有出现问题。我们的“欺骗”行为没有被揭穿,异常成功。:)