哭笑不得的IE Bug

作者: Jeffrey Zhao  來源: 博客園  發布時間: 2010-03-23 15:17  閱讀: 788 次  推薦: 0   原文鏈接   [收藏]  

還記得《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類庫。這個實現非常容易實現的,我簡單介紹一下它的使用方式。

  1. 在需要發起AJAX請求時,不能直接調用最后的方法來發起請求。需要封裝一個delegate然后放入隊列。
  2. 在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,而不會判斷它到底會不會出錯。這也說明,這個狀況本身已經被那些類庫所忽略了。

那么我們也忽略一下吧,這個解決方案還是比較讓人滿意的。至少目前看來,在使用過程中沒有出現問題。我們的“欺騙”行為沒有被揭穿,異常成功。:)

0
0
 
標簽:IE Bug
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()