代码改变世界

挣脱浏览器的束缚(5) - 哭笑不得的IE Bug

2007-01-27 03:27  Jeffrey Zhao  阅读(7234)  评论(76编辑  收藏  举报

还记得《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 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,而不会判断它到底会不会出错。这也说明,这个状况本身已经被那些类库所忽略了。

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