65.9K
CodeProject 正在变化。 阅读更多。
Home

Internet Explorer 请求过多时会卡住?伪造 XHR!

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.57/5 (3投票s)

2007年6月21日

CPOL

5分钟阅读

viewsIcon

43634

downloadIcon

190

当页面同时建立过多连接时,Internet Explorer 会卡死。让我们构建一个伪造的 XHR 对象来解决 Internet Explorer 浏览器的这个 bug。

引言

几个月前,我的一位朋友,他也是一名顾问和培训师,告诉我他的一个客户遇到了一个问题。当页面同时建立过多连接时,Internet Explorer 会卡死。由于 AJAX 技术如今已广泛使用,这个问题变得越来越普遍。当一个 AJAX 应用程序由较小的应用程序组成时——我们称之为“mash up”——这个问题很可能会发生。

这是 Internet Explorer 的一个 bug。当您发出大量 AJAX 调用时,浏览器会将所有请求保存在一个队列中,一次执行两个。所以,如果您点击某个东西尝试导航到另一个页面,浏览器必须等待正在运行的调用完成,然后才能处理下一个。这个 bug 在 Internet Explorer 6 中非常严重,不幸的是,它在 Internet Explorer 7 中仍然存在。

以编程方式管理请求

解决方案很简单。我们应该自己维护队列,并逐个将请求发送到浏览器的队列。因此,我编写了一个队列来管理请求。这真的很简单

if (!window.Global)
{
    window.Global = new Object();
}

Global._ConnectionManager = function()
{
    this._requestDelegateQueue = new Array();    
    this._requestInProgress = 0;    
    this._maxConcurrentRequest = 2;
}

Global._ConnectionManager.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.ConnectionManager = new Global._ConnectionManager();

我使用纯 JavaScript 代码构建了一个名为 ConnectionManager 的组件,没有任何 AJAX/JavaScript 框架/库的依赖。如果用户想使用此组件来管理请求,他们应该使用 enqueueRequestDelegate 方法将一个代理放入队列。当浏览器中没有正在进行的请求或只有一个正在进行的请求时,将执行该代理。在从服务器接收到响应后,用户必须调用 next 方法来通知 ConnectionManager,然后 ConnectionManager 将执行队列中的下一个待处理请求代理(如果队列不为空)。

例如,如果我们使用 Prototype 框架连续进行十次 AJAX 调用

function requestWithoutQueue()
{
    for (var i = 0; i < 10; i++)
    {
        new Ajax.Request(
            url,
            {
                method: 'post',
                onComplete: callback
            });
    }
}
    
function callback(xmlHttpRequest)
{
    // do something
}

我们将使用 ConnectionManager 来排队请求,如下所示:

function requestWithQueue()
{
    for (var i = 0; i < 10; i++)
    {
        var requestDelegate = function()
        {
            new Ajax.Request(
                url,
                {
                    method: 'post',
                    onComplete: callback,
                    onFailure: Global.ConnectionManager.next,
                    onException: Global.ConnectionManager.next
                });
        }
        
        Global.ConnectionManager.enqueueRequestDelegate(requestDelegate);
    }    
}

function callback(xmlHttpRequest)
{
    // do sth.
    Global.ConnectionManager.next();
}

请注意,我们将 next 方法分配给 onFailureonException 回调处理程序,以确保在从服务器接收到响应后会调用它,因为队列中的其余代理将无法执行,并且如果 next 方法尚未执行,系统将无法发出新的调用。

我把文件发给了我的朋友,几天后他告诉我他的客户说这个组件很难用。我同意。它确实冗长且容易出错。显然,ConnectionManager 不太方便集成到现有代码中。开发人员必须确保所有请求都在 ConnectionManager 中排队,并且无论请求何时完成,都必须在任何情况下执行 next 方法。但这还远远不够。越来越多的 AJAX 应用程序将执行服务器创建的脚本。如果客户端的互联网连接不够稳定,动态创建的文件可能无法成功加载。届时,脚本执行会抛出异常,而按设计本应执行的 next 方法很可能会被错过。

构建一个伪造的 XMLHttpRequest 类型

经过几天思考,我得到一个主意。如果我们能使用另一个组件来替换原生的 XMLHttpRequest 对象并提供内置的请求队列,那将是完美的。如果这样,开发人员就可以通过在页面中放置脚本文件来解决问题,而无需更改一行代码。

解决方案比我之前想象的要容易得多,现在我将向您展示如何构建它。

我们首先要做的就是保留原生的 XHR 类型。请注意,以下代码已经解决了不同浏览器中 XHR 的兼容性问题。

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 _originaActiveXObject(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;

然后,我们应该创建一个新类来替换原生的 XHR 类型。大多数方法只是委托给原生对象中的相应方法。

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;
    },
    
    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();
    },
    
    _createDelegate : function(instance, method)
    {
        return function()
        {
            return method.apply(instance, arguments);
        }
    },
    
    _internalOnReadyStateChange : function()
    {
        // ...
    },
    
    send : function(body)
    {
        // ...
    }
}

关键点在于 send 方法和 _internalOnReadyStateChange 方法的实现。send 方法会将一个原生 XHR 类型方法的代理放入队列。该代理将在适当的时间由 ConnectionManager 执行。

send : function(body)
{
    var requestDelegate = this._createDelegate(
        this,
        function()
        {
            this._xmlHttpRequest.send(body);
            this.readyState = this._xmlHttpRequest.readyState;
        });
    
    Global.ConnectionManager.enqueueRequestDelegate(requestDelegate);
},

我们在构造函数中将 _internalOnReadyStateChange 方法作为原生 XHR 对象的 onreadystatechange 回调处理程序。当回调函数触发时,我们将保留原生对象的所有属性,并执行我们的 onreadystatechange 处理程序。请注意,我们的新组件负责在 readyState 等于 4(表示当前请求“已完成”)时执行 ConnectionManagernext 方法,以便从开发者的角度来看,next 方法可以自动执行。

_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.ConnectionManager.next();
    }
    
    if (this.onreadystatechange)
    {
        this.onreadystatechange.call(null);
    }
}

我们已尽最大努力使新组件的行为与原生 XHR 类型相同。但它仍然存在。这是一个小问题,但我们无法做到。当我们在原生 XHR 对象中访问 status 属性时,如果对象无法从服务器端接收到标头,就会抛出错误。但在 Internet Explorer 中,我们无法使用 FireFox 中的 __setter__ 关键字将对象的属性定义为方法。这是使用这两个组件时原生 XHR 类型与我们的新类型之间的唯一区别。

如何使用

现在,我们可以轻松地在页面中引用 JS 文件来解决用户使用 Internet Explorer 浏览页面时遇到的问题。

<!--[if IE]>
    <script type="text/javascript" src="ConnectionManager.js"></script>
    <script type="text/javascript" src="MyXMLHttpRequest.js"></script>
<![endif]-->

我把脚本文件发给了我的朋友。他的客户似乎对这个解决方案很满意。

历史

  • 2007 年 6 月 21 日:初次发布
© . All rights reserved.