哭笑不得的IE Bug
還記得《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 Queuetitle> <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,而不會判斷它到底會不會出錯。這也說明,這個狀況本身已經被那些類庫所忽略了。
那么我們也忽略一下吧,這個解決方案還是比較讓人滿意的。至少目前看來,在使用過程中沒有出現問題。我們的“欺騙”行為沒有被揭穿,異常成功。:)