挣脱浏览器的束缚(5) - 哭笑不得的IE Bug
2007-01-27 03:27 by 老赵, 7819 visits还记得《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》一文中一样,最容易想到的解决方案就是编写代码来维护一个队列。这个队列非常容易编写,代码如下:
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版时我们可以这样:
<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,太容易骗了。话不多说,直接来看代码,一目了然:
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,而不会判断它到底会不会出错。这也说明,这个状况本身已经被那些类库所忽略了。
那么我们也忽略一下吧,这个解决方案还是比较让人满意的。至少目前看来,在使用过程中没有出现问题。我们的“欺骗”行为没有被揭穿,异常成功。:)
上面的代码使用了框架无关的实现,能够轻松地兼容各种JavaScript类库。完全可以直接拿过来使用,当时写完之后也非常的高兴,蛮有成就感的。:)